From b7850a6c64f94409365f23a625696d6711832e94 Mon Sep 17 00:00:00 2001 From: Weiko Date: Wed, 24 Jun 2026 11:47:44 +0200 Subject: [PATCH] feat(metadata): deterministic universalIdentifiers for server-generated side-effects (#21949) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Context Server-generated "side-effect" entities created for every object (system fields, INDEX view, record-page fields view + view fields, search-vector index, navigation command, record page layout/tabs/widgets) were minted with random v4() ids. Because they were non-deterministic, nothing could reference them by id (e.g. point a view field at an object's createdAt field). This PR introduces a single shared rule for deriving these ids deterministically via uuid v5, so the same (owner app, parent, kind) always yields the same id, making side-effects referable and reproducible. This is the **forward-only foundation** (PR1). Follow-ups: - PR2: SDK with optional universalIdentifier + expose helpers to app authors. - PR3: regenerate the standard-app constants to the same scheme + workspace backfill. ## The rule ```ts universalIdentifier = computeOwnerScopedUniversalIdentifier({ ownerAppUID, namespace, value }) = v5(value, v5(ownerAppUID, ENTITY_TYPE_NAMESPACE)) value = `${parentUID}:${discriminator}` // entity scoped under a parent = `${discriminator}` // top-level, app-parented entity ``` - ownerAppUID: The application that owns the entity (already threaded through every generator as applicationUniversalIdentifier); folded into the namespace so it both owns and scopes the id — two apps adding the same-named entity to a shared parent never collide. - namespace: Per entity type (ENTITY_TYPE_NAMESPACE_BY_TYPE), so different types with the same parent+discriminator never collide. - parentUID: The immediate parent's actual universalIdentifier (omitted for top-level entities, since the owner app already scopes them). - discriminator: A stable semantic key (field name, tab/widget title, generated index name, select-option value, …). Scope boundary: deterministic v5 applies to system side-effects (unique by construction) and, later, app-authored manifest entities (uniqueness enforced at SDK build time). Entities created through the UI by the workspace "Custom" app (custom objects/views/fields) keep v4, their natural keys aren't unique and aren't enforced. A UI-created custom object keeps its v4 id; its side-effects are deterministic relative to that v4 parent. Changes twenty-shared: new application/deterministic-identifier/ module: - computeDeterministicUuid(value, namespace) primitive + a thin computeOwnerScopedUniversalIdentifier wrapper (boilerplate only), and frozen ENTITY_TYPE_NAMESPACE_BY_TYPE. - One self-contained util per usecase (no central registry, no generic engine): each util bakes in its own discriminator + namespace, so a key lives next to the code that uses it and is individually testable. ~28 utils covering side-effect and (future) app-authored entities, e.g. getFieldUniversalIdentifier, getIndexViewUniversalIdentifier, getFieldsWidgetViewUniversalIdentifier, getViewFieldUniversalIdentifier, getIndexUniversalIdentifier, getRecordPageLayoutUniversalIdentifier, getPageLayoutTab/WidgetUniversalIdentifier, getNavigationCommandUniversalIdentifier, plus the general getViewUniversalIdentifier / getPageLayoutUniversalIdentifier and app-authored getObject/Role/PermissionFlag/Agent/Skill/…UniversalIdentifier. - Golden snapshot test locking every util's output for fixed inputs, plus a cross-type no-collision test. twenty-server: side-effect generators now derive universalIdentifier via the helpers (local id PKs stay v4()): system fields + name, INDEX view, record-page fields (fields-widget) view, default view fields, search-vector index, nav command, page layout/tabs/widgets. Index ids key off the generated Postgres index name; extracted computeFlatIndexNameOrThrow so the name (and therefore the id) is computed once with no placeholder. ## Timeline ### What actually changes - New objects (custom objects created via Settings/metadata API) and fresh standard installs now get deterministic v5 universalIdentifiers for all side-effect entities (system fields, views, view fields, search index, nav command, page layout/tabs/widgets) instead of random v4(). - The nav-command id formula changed (new owner-scoped) for new objects, fresh standard installs, and the runtime lookup. ### What does NOT change - Existing objects' side-effect ids — untouched (no migration; forward-only). - Standard object UIDs — untouched - UI-created custom entities' own ids stay v4 (see scope boundary above). - Fresh installs are behaviorally a no-op — ids are internal; re-sync produces no diff (verified). Nothing user-visible. ### The one real-world impact / risk (existing workspaces) The nav-command runtime lookup (findNavigationCommandMenuItemForObject) now computes the new formula, but existing workspaces' nav commands were stored with the old formula. So on an upgraded existing workspace, until the PR3 backfill: - Object activate/deactivate toggle for existing objects won't find the nav command → re-activating can create a duplicate nav command; deactivating may no-op. - Object deletion won't find/clean up the old nav command → orphaned nav-command row. ### What app developers get right now Nothing usable yet. The helpers exist in twenty-shared but aren't re-exported from twenty-sdk (PR2), and app-authored objects still get SDK-derived ids in the old format until PR2 re-mints them. So "reference a server entity by deterministic id" doesn't work end-to-end until PR2 --- ...00-refactor-navigation-commands.command.ts | 1 + ...00-backfill-record-page-layouts.command.ts | 33 ++--- ...menu-item-operations-or-throw.util.spec.ts | 4 + ...mand-menu-item-operations-or-throw.util.ts | 1 + ...gation-flat-command-menu-item.util.spec.ts | 14 +-- ...-navigation-flat-command-menu-item.util.ts | 8 +- .../compute-flat-index-name-or-throw.util.ts | 101 +++++++++++++++ ...deterministic-universal-identifier.util.ts | 63 ++++++++++ .../utils/generate-flat-index.util.ts | 79 +----------- .../object-metadata.service.ts | 100 +++++---------- ...te-flat-view-fields-to-create.util.spec.ts | 2 +- ...-field-metadatas-for-custom-object.util.ts | 31 +++-- ...ld-default-index-for-custom-object.util.ts | 62 ++++------ ...fault-record-page-layout-to-create.util.ts | 115 +++++++++++++----- ...-record-page-fields-view-to-create.util.ts | 8 +- ...compute-flat-view-fields-to-create.util.ts | 8 +- ...andard-flat-command-menu-item-maps.util.ts | 10 +- ...et-agent-universal-identifier.util.spec.ts | 14 +++ ...variable-universal-identifier.util.spec.ts | 14 +++ ...enu-item-universal-identifier.util.spec.ts | 29 +++++ ...provider-universal-identifier.util.spec.ts | 14 +++ ...rmission-universal-identifier.util.spec.ts | 17 +++ ...et-field-universal-identifier.util.spec.ts | 16 +++ ...omponent-universal-identifier.util.spec.ts | 14 +++ ...et-index-universal-identifier.util.spec.ts | 16 +++ ...function-universal-identifier.util.spec.ts | 14 +++ ...enu-item-universal-identifier.util.spec.ts | 14 +++ ...rmission-universal-identifier.util.spec.ts | 17 +++ ...t-object-universal-identifier.util.spec.ts | 14 +++ ...yout-tab-universal-identifier.util.spec.ts | 16 +++ ...e-layout-universal-identifier.util.spec.ts | 39 ++++++ ...t-widget-universal-identifier.util.spec.ts | 16 +++ ...ion-flag-universal-identifier.util.spec.ts | 14 +++ ...ion-flag-universal-identifier.util.spec.ts | 17 +++ ...get-role-universal-identifier.util.spec.ts | 14 +++ ...t-option-universal-identifier.util.spec.ts | 16 +++ ...et-skill-universal-identifier.util.spec.ts | 14 +++ ...ew-field-universal-identifier.util.spec.ts | 17 +++ ...w-filter-universal-identifier.util.spec.ts | 19 +++ ...ew-group-universal-identifier.util.spec.ts | 16 +++ ...iew-sort-universal-identifier.util.spec.ts | 17 +++ ...get-view-universal-identifier.util.spec.ts | 43 +++++++ .../compute-deterministic-uuid.util.ts | 13 ++ .../deterministic-entity-namespace.type.ts | 4 + .../get-agent-universal-identifier.util.ts | 15 +++ ...tion-variable-universal-identifier.util.ts | 15 +++ ...and-menu-item-universal-identifier.util.ts | 32 +++++ ...tion-provider-universal-identifier.util.ts | 15 +++ ...ld-permission-universal-identifier.util.ts | 17 +++ .../get-field-universal-identifier.util.ts | 17 +++ ...ont-component-universal-identifier.util.ts | 15 +++ .../get-index-universal-identifier.util.ts | 18 +++ ...ogic-function-universal-identifier.util.ts | 15 +++ ...ion-menu-item-universal-identifier.util.ts | 15 +++ ...ct-permission-universal-identifier.util.ts | 17 +++ .../get-object-universal-identifier.util.ts | 15 +++ ...ge-layout-tab-universal-identifier.util.ts | 17 +++ ...t-page-layout-universal-identifier.util.ts | 35 ++++++ ...layout-widget-universal-identifier.util.ts | 17 +++ ...rmission-flag-universal-identifier.util.ts | 15 +++ ...rmission-flag-universal-identifier.util.ts | 17 +++ .../get-role-universal-identifier.util.ts | 15 +++ ...select-option-universal-identifier.util.ts | 17 +++ .../get-skill-universal-identifier.util.ts | 15 +++ ...et-view-field-universal-identifier.util.ts | 17 +++ ...t-view-filter-universal-identifier.util.ts | 21 ++++ ...et-view-group-universal-identifier.util.ts | 17 +++ ...get-view-sort-universal-identifier.util.ts | 17 +++ .../get-view-universal-identifier.util.ts | 48 ++++++++ .../twenty-shared/src/application/index.ts | 37 ++++++ 70 files changed, 1358 insertions(+), 261 deletions(-) create mode 100644 packages/twenty-server/src/engine/metadata-modules/index-metadata/utils/compute-flat-index-name-or-throw.util.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/index-metadata/utils/generate-flat-index-metadata-with-deterministic-universal-identifier.util.ts create mode 100644 packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-agent-universal-identifier.util.spec.ts create mode 100644 packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-application-variable-universal-identifier.util.spec.ts create mode 100644 packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-command-menu-item-universal-identifier.util.spec.ts create mode 100644 packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-connection-provider-universal-identifier.util.spec.ts create mode 100644 packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-field-permission-universal-identifier.util.spec.ts create mode 100644 packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-field-universal-identifier.util.spec.ts create mode 100644 packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-front-component-universal-identifier.util.spec.ts create mode 100644 packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-index-universal-identifier.util.spec.ts create mode 100644 packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-logic-function-universal-identifier.util.spec.ts create mode 100644 packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-navigation-menu-item-universal-identifier.util.spec.ts create mode 100644 packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-object-permission-universal-identifier.util.spec.ts create mode 100644 packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-object-universal-identifier.util.spec.ts create mode 100644 packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-page-layout-tab-universal-identifier.util.spec.ts create mode 100644 packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-page-layout-universal-identifier.util.spec.ts create mode 100644 packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-page-layout-widget-universal-identifier.util.spec.ts create mode 100644 packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-permission-flag-universal-identifier.util.spec.ts create mode 100644 packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-role-permission-flag-universal-identifier.util.spec.ts create mode 100644 packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-role-universal-identifier.util.spec.ts create mode 100644 packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-select-option-universal-identifier.util.spec.ts create mode 100644 packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-skill-universal-identifier.util.spec.ts create mode 100644 packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-view-field-universal-identifier.util.spec.ts create mode 100644 packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-view-filter-universal-identifier.util.spec.ts create mode 100644 packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-view-group-universal-identifier.util.spec.ts create mode 100644 packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-view-sort-universal-identifier.util.spec.ts create mode 100644 packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-view-universal-identifier.util.spec.ts create mode 100644 packages/twenty-shared/src/application/deterministic-identifier/compute-deterministic-uuid.util.ts create mode 100644 packages/twenty-shared/src/application/deterministic-identifier/deterministic-entity-namespace.type.ts create mode 100644 packages/twenty-shared/src/application/deterministic-identifier/get-agent-universal-identifier.util.ts create mode 100644 packages/twenty-shared/src/application/deterministic-identifier/get-application-variable-universal-identifier.util.ts create mode 100644 packages/twenty-shared/src/application/deterministic-identifier/get-command-menu-item-universal-identifier.util.ts create mode 100644 packages/twenty-shared/src/application/deterministic-identifier/get-connection-provider-universal-identifier.util.ts create mode 100644 packages/twenty-shared/src/application/deterministic-identifier/get-field-permission-universal-identifier.util.ts create mode 100644 packages/twenty-shared/src/application/deterministic-identifier/get-field-universal-identifier.util.ts create mode 100644 packages/twenty-shared/src/application/deterministic-identifier/get-front-component-universal-identifier.util.ts create mode 100644 packages/twenty-shared/src/application/deterministic-identifier/get-index-universal-identifier.util.ts create mode 100644 packages/twenty-shared/src/application/deterministic-identifier/get-logic-function-universal-identifier.util.ts create mode 100644 packages/twenty-shared/src/application/deterministic-identifier/get-navigation-menu-item-universal-identifier.util.ts create mode 100644 packages/twenty-shared/src/application/deterministic-identifier/get-object-permission-universal-identifier.util.ts create mode 100644 packages/twenty-shared/src/application/deterministic-identifier/get-object-universal-identifier.util.ts create mode 100644 packages/twenty-shared/src/application/deterministic-identifier/get-page-layout-tab-universal-identifier.util.ts create mode 100644 packages/twenty-shared/src/application/deterministic-identifier/get-page-layout-universal-identifier.util.ts create mode 100644 packages/twenty-shared/src/application/deterministic-identifier/get-page-layout-widget-universal-identifier.util.ts create mode 100644 packages/twenty-shared/src/application/deterministic-identifier/get-permission-flag-universal-identifier.util.ts create mode 100644 packages/twenty-shared/src/application/deterministic-identifier/get-role-permission-flag-universal-identifier.util.ts create mode 100644 packages/twenty-shared/src/application/deterministic-identifier/get-role-universal-identifier.util.ts create mode 100644 packages/twenty-shared/src/application/deterministic-identifier/get-select-option-universal-identifier.util.ts create mode 100644 packages/twenty-shared/src/application/deterministic-identifier/get-skill-universal-identifier.util.ts create mode 100644 packages/twenty-shared/src/application/deterministic-identifier/get-view-field-universal-identifier.util.ts create mode 100644 packages/twenty-shared/src/application/deterministic-identifier/get-view-filter-universal-identifier.util.ts create mode 100644 packages/twenty-shared/src/application/deterministic-identifier/get-view-group-universal-identifier.util.ts create mode 100644 packages/twenty-shared/src/application/deterministic-identifier/get-view-sort-universal-identifier.util.ts create mode 100644 packages/twenty-shared/src/application/deterministic-identifier/get-view-universal-identifier.util.ts 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 429abd1738..72ec0961e5 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,6 +202,7 @@ 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 2e5f185049..1367d0a1b7 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,8 +13,6 @@ 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'; @@ -508,39 +506,32 @@ 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 viewFields = computeFlatViewFieldsToCreate({ - objectFlatFieldMetadatas: objectFieldMetadatas, - viewUniversalIdentifier: fieldsView.universalIdentifier, + const { + pageLayouts, + pageLayoutTabs, + pageLayoutWidgets, + recordPageFieldsView, + recordPageFieldsViewFields, + } = computeFlatDefaultRecordPageLayoutToCreate({ + objectMetadata: customObject, flatApplication: twentyStandardFlatApplication, + objectFlatFieldMetadatas: objectFieldMetadatas, labelIdentifierFieldMetadataUniversalIdentifier: customObject.labelIdentifierFieldMetadataUniversalIdentifier, - excludeLabelIdentifier: true, + workspaceId, }); - const { pageLayouts, pageLayoutTabs, pageLayoutWidgets } = - computeFlatDefaultRecordPageLayoutToCreate({ - objectMetadata: customObject, - flatApplication: twentyStandardFlatApplication, - recordPageFieldsView: fieldsView, - workspaceId, - }); - allPageLayouts.push(...pageLayouts); allTabs.push(...pageLayoutTabs); allWidgets.push(...pageLayoutWidgets); - allViews.push(fieldsView); - allViewFields.push(...viewFields); + allViews.push(recordPageFieldsView); + allViewFields.push(...recordPageFieldsViewFields); } 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 35cae2d219..fff9d6d8df 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,6 +57,10 @@ 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 b0bb1458ba..0d3838c399 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,6 +69,7 @@ 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 7f0279d217..f76412f34b 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,5 +1,4 @@ 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'; @@ -11,9 +10,6 @@ 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', @@ -29,18 +25,14 @@ const baseArgs = { workspaceId: 'ws-id-1', position: 5, now: '2026-01-01T00:00:00.000Z', + universalIdentifier: 'nav-universal-1', }; describe('buildNavigationFlatCommandMenuItem', () => { - it('should produce a deterministic universalIdentifier via UUID v5', () => { + it('should use the provided universalIdentifier', () => { const result = buildNavigationFlatCommandMenuItem(baseArgs); - const expectedUniversalIdentifier = v5( - baseObjectMetadata.universalIdentifier, - NAVIGATION_COMMAND_UUID_NAMESPACE, - ); - - expect(result.universalIdentifier).toBe(expectedUniversalIdentifier); + expect(result.universalIdentifier).toBe('nav-universal-1'); }); 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 8120c89dfe..e872b8a479 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,7 +1,6 @@ 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'; @@ -54,6 +53,7 @@ export const buildNavigationFlatCommandMenuItem = ({ workspaceId, position, now, + universalIdentifier, }: { objectMetadata: { id: string; @@ -67,12 +67,8 @@ 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 new file mode 100644 index 0000000000..797bb71f9f --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/index-metadata/utils/compute-flat-index-name-or-throw.util.ts @@ -0,0 +1,101 @@ +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 new file mode 100644 index 0000000000..0a693f61c1 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/index-metadata/utils/generate-flat-index-metadata-with-deterministic-universal-identifier.util.ts @@ -0,0 +1,63 @@ +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 d6b78aef58..c1ca5bfd4d 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,15 +1,4 @@ -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 { 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'; @@ -25,69 +14,11 @@ export const generateFlatIndexMetadataWithNameOrThrow = ({ objectFlatFieldMetadatas, flatIndex, }: GenerateFlatIndexArgs): UniversalFlatIndexMetadata => { - 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({ + const name = computeFlatIndexNameOrThrow({ flatObjectMetadata, - orderedIndexColumnNames, + objectFlatFieldMetadatas, + universalFlatIndexFieldMetadatas: + flatIndex.universalFlatIndexFieldMetadatas, 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 74f76c5201..7df78ee864 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,6 +2,10 @@ 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, @@ -10,15 +14,12 @@ import { } from 'twenty-shared/types'; import { fromArrayToUniqueKeyRecord, isDefined } from 'twenty-shared/utils'; import { FindManyOptions, FindOneOptions, Repository } from 'typeorm'; -import { v4, v5 } from 'uuid'; +import { v4 } 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, - NAVIGATION_COMMAND_UUID_NAMESPACE, -} from 'src/engine/metadata-modules/flat-command-menu-item/utils/build-navigation-flat-command-menu-item.util'; +import { buildNavigationFlatCommandMenuItem } 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'; @@ -30,9 +31,6 @@ 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'; @@ -43,7 +41,6 @@ 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'; @@ -420,6 +417,8 @@ export class ObjectMetadataService extends TypeOrmQueryService; }; }): FlatCommandMenuItem | undefined { - const commandMenuItemUniversalIdentifier = v5( - objectUniversalIdentifier, - NAVIGATION_COMMAND_UUID_NAMESPACE, - ); + const commandMenuItemUniversalIdentifier = + getNavigationCommandUniversalIdentifier({ + applicationUniversalIdentifier, + objectUniversalIdentifier, + }); return findFlatEntityByUniversalIdentifier({ flatEntityMaps: flatCommandMenuItemMaps, @@ -919,6 +877,8 @@ export class ObjectMetadataService extends TypeOrmQueryService + getFieldUniversalIdentifier({ + applicationUniversalIdentifier, + objectUniversalIdentifier: objectMetadataUniversalIdentifier, + name: fieldName, + }); + return { id: { ...id, - universalIdentifier: v4(), + universalIdentifier: getSystemFieldUniversalIdentifier(id.name), applicationUniversalIdentifier, objectMetadataUniversalIdentifier, createdAt: now, @@ -51,7 +58,7 @@ const buildObjectSystemFlatFieldMetadatas = ({ }, createdAt: { ...createdAt, - universalIdentifier: v4(), + universalIdentifier: getSystemFieldUniversalIdentifier(createdAt.name), applicationUniversalIdentifier, objectMetadataUniversalIdentifier, createdAt: now, @@ -59,7 +66,7 @@ const buildObjectSystemFlatFieldMetadatas = ({ }, createdBy: { ...createdBy, - universalIdentifier: v4(), + universalIdentifier: getSystemFieldUniversalIdentifier(createdBy.name), applicationUniversalIdentifier, objectMetadataUniversalIdentifier, createdAt: now, @@ -67,7 +74,7 @@ const buildObjectSystemFlatFieldMetadatas = ({ }, deletedAt: { ...deletedAt, - universalIdentifier: v4(), + universalIdentifier: getSystemFieldUniversalIdentifier(deletedAt.name), applicationUniversalIdentifier, objectMetadataUniversalIdentifier, createdAt: now, @@ -75,7 +82,7 @@ const buildObjectSystemFlatFieldMetadatas = ({ }, position: { ...position, - universalIdentifier: v4(), + universalIdentifier: getSystemFieldUniversalIdentifier(position.name), applicationUniversalIdentifier, objectMetadataUniversalIdentifier, createdAt: now, @@ -83,7 +90,7 @@ const buildObjectSystemFlatFieldMetadatas = ({ }, searchVector: { ...searchVector, - universalIdentifier: v4(), + universalIdentifier: getSystemFieldUniversalIdentifier(searchVector.name), applicationUniversalIdentifier, objectMetadataUniversalIdentifier, createdAt: now, @@ -92,7 +99,7 @@ const buildObjectSystemFlatFieldMetadatas = ({ }, updatedAt: { ...updatedAt, - universalIdentifier: v4(), + universalIdentifier: getSystemFieldUniversalIdentifier(updatedAt.name), applicationUniversalIdentifier, objectMetadataUniversalIdentifier, createdAt: now, @@ -100,7 +107,7 @@ const buildObjectSystemFlatFieldMetadatas = ({ }, updatedBy: { ...updatedBy, - universalIdentifier: v4(), + universalIdentifier: getSystemFieldUniversalIdentifier(updatedBy.name), applicationUniversalIdentifier, objectMetadataUniversalIdentifier, createdAt: now, @@ -126,7 +133,11 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({ type: FieldMetadataType.TEXT, isLabelSyncedWithName: false, isUnique: false, - universalIdentifier: v4(), + universalIdentifier: getFieldUniversalIdentifier({ + applicationUniversalIdentifier, + objectUniversalIdentifier: objectMetadataUniversalIdentifier, + name: 'name', + }), 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 bae4cf6def..ab7977bf28 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,7 +1,5 @@ -import { v4 } from 'uuid'; - import { IndexType } from 'src/engine/metadata-modules/index-metadata/types/indexType.types'; -import { generateFlatIndexMetadataWithNameOrThrow } from 'src/engine/metadata-modules/index-metadata/utils/generate-flat-index.util'; +import { generateFlatIndexMetadataWithDeterministicUniversalIdentifierOrThrow } from 'src/engine/metadata-modules/index-metadata/utils/generate-flat-index-metadata-with-deterministic-universal-identifier.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'; @@ -16,39 +14,33 @@ export const buildDefaultIndexesForCustomObject = ({ objectFlatFieldMetadatas: UniversalFlatFieldMetadata[]; defaultFlatFieldForCustomObjectMaps: DefaultFlatFieldForCustomObjectMaps; }) => { - 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 createdAt = new Date().toISOString(); - indexType: IndexType.GIN, - indexWhereClause: null, - isCustom: false, - isUnique: false, - isSystemSideEffect: true, - objectMetadataUniversalIdentifier: flatObjectMetadata.universalIdentifier, - universalIdentifier: tsFlatVectorIndexUniversalIdentifier, - updatedAt: createdAt.toISOString(), - applicationUniversalIdentifier: - flatObjectMetadata.applicationUniversalIdentifier, - }, - flatObjectMetadata, - }); + 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, + }, + }); 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 5dd6b4a758..0e7ece4ede 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,36 +1,52 @@ +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, - recordPageFieldsView, + objectFlatFieldMetadatas, + labelIdentifierFieldMetadataUniversalIdentifier, workspaceId, }: { flatApplication: FlatApplication; objectMetadata: UniversalFlatObjectMetadata & { id: string }; - recordPageFieldsView: UniversalFlatView & { id: string }; + objectFlatFieldMetadatas: UniversalFlatFieldMetadata[]; + labelIdentifierFieldMetadataUniversalIdentifier: string | null; workspaceId: string; }): { pageLayouts: FlatPageLayout[]; pageLayoutTabs: FlatPageLayoutTab[]; pageLayoutWidgets: FlatPageLayoutWidget[]; + recordPageFieldsView: UniversalFlatView & { id: string }; + recordPageFieldsViewFields: UniversalFlatViewField[]; } => { const now = new Date().toISOString(); const pageLayoutId = v4(); - const pageLayoutUniversalIdentifier = v4(); + const pageLayoutUniversalIdentifier = getRecordPageLayoutUniversalIdentifier({ + applicationUniversalIdentifier: flatApplication.universalIdentifier, + objectUniversalIdentifier: objectMetadata.universalIdentifier, + }); const tabDefinitions = [ { key: 'home' as const, widgetKey: 'fields' as const }, @@ -42,14 +58,24 @@ 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 = v4(); + const tabUniversalIdentifier = getPageLayoutTabUniversalIdentifier({ + applicationUniversalIdentifier: flatApplication.universalIdentifier, + pageLayoutUniversalIdentifier, + title: tabProps.title, + }); const widgetId = v4(); - const widgetUniversalIdentifier = v4(); + const widgetUniversalIdentifier = getPageLayoutWidgetUniversalIdentifier({ + applicationUniversalIdentifier: flatApplication.universalIdentifier, + pageLayoutTabUniversalIdentifier: tabUniversalIdentifier, + title: widgetProps.title, + }); pageLayoutTabs.push({ id: tabId, @@ -75,31 +101,48 @@ export const computeFlatDefaultRecordPageLayoutToCreate = ({ const isFieldsWidget = widgetKey === 'fields'; - const configuration = isFieldsWidget - ? { - configurationType: WidgetConfigurationType.FIELDS, - viewId: recordPageFieldsView.id, - newFieldDefaultVisibility: true, - } - : { - configurationType: - WidgetConfigurationType[ - widgetKey.toUpperCase() as keyof typeof WidgetConfigurationType - ], - }; + if (isFieldsWidget) { + recordPageFieldsView = computeFlatRecordPageFieldsViewToCreate({ + objectMetadata, + flatApplication, + pageLayoutWidgetUniversalIdentifier: widgetUniversalIdentifier, + }); + recordPageFieldsViewFields = computeFlatViewFieldsToCreate({ + flatApplication, + objectFlatFieldMetadatas, + labelIdentifierFieldMetadataUniversalIdentifier, + viewUniversalIdentifier: recordPageFieldsView.universalIdentifier, + excludeLabelIdentifier: true, + }); + } - const universalConfiguration = isFieldsWidget - ? { - configurationType: WidgetConfigurationType.FIELDS, - viewUniversalIdentifier: recordPageFieldsView.universalIdentifier, - 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 + ], + }; pageLayoutWidgets.push({ id: widgetId, @@ -129,6 +172,12 @@ 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, @@ -151,5 +200,11 @@ export const computeFlatDefaultRecordPageLayoutToCreate = ({ defaultTabToFocusOnMobileAndSidePanelUniversalIdentifier: null, }; - return { pageLayouts: [pageLayout], pageLayoutTabs, pageLayoutWidgets }; + return { + pageLayouts: [pageLayout], + pageLayoutTabs, + pageLayoutWidgets, + recordPageFieldsView, + recordPageFieldsViewFields, + }; }; 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 4ea594b8e6..721ae66d0a 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,3 +1,4 @@ +import { getFieldsWidgetViewUniversalIdentifier } from 'twenty-shared/application'; import { ViewOpenRecordIn, ViewType, @@ -12,9 +13,11 @@ 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(); @@ -40,7 +43,10 @@ export const computeFlatRecordPageFieldsViewToCreate = ({ mainGroupByFieldMetadataUniversalIdentifier: null, openRecordIn: ViewOpenRecordIn.SIDE_PANEL, position: 0, - universalIdentifier: v4(), + universalIdentifier: getFieldsWidgetViewUniversalIdentifier({ + applicationUniversalIdentifier: flatApplication.universalIdentifier, + pageLayoutWidgetUniversalIdentifier, + }), 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 c30049ace1..e238e71434 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 { v4 } from 'uuid'; +import { getViewFieldUniversalIdentifier } from 'twenty-shared/application'; import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type'; import { DEFAULT_VIEW_FIELD_SIZE } from 'src/engine/metadata-modules/flat-view-field/constants/default-view-field-size.constant'; @@ -59,7 +59,11 @@ export const computeFlatViewFieldsToCreate = ({ createdAt, updatedAt: createdAt, deletedAt: null, - universalIdentifier: v4(), + universalIdentifier: getViewFieldUniversalIdentifier({ + applicationUniversalIdentifier: flatApplication.universalIdentifier, + viewUniversalIdentifier, + fieldMetadataUniversalIdentifier: field.universalIdentifier, + }), 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 fcc92c67d7..65e990aba2 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,4 +1,7 @@ -import { TWENTY_STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER } from 'twenty-shared/application'; +import { + getNavigationCommandUniversalIdentifier, + TWENTY_STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER, +} from 'twenty-shared/application'; import { isDefined } from 'twenty-shared/utils'; import { v4 } from 'uuid'; @@ -78,6 +81,11 @@ 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-agent-universal-identifier.util.spec.ts b/packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-agent-universal-identifier.util.spec.ts new file mode 100644 index 0000000000..7cd09600ea --- /dev/null +++ b/packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-agent-universal-identifier.util.spec.ts @@ -0,0 +1,14 @@ +import { getAgentUniversalIdentifier } from '@/application/deterministic-identifier/get-agent-universal-identifier.util'; + +const APP = '11111111-1111-4111-8111-111111111111'; + +describe('getAgentUniversalIdentifier', () => { + it('derives a deterministic id from the agent name within its application', () => { + expect( + getAgentUniversalIdentifier({ + applicationUniversalIdentifier: APP, + name: 'My Agent', + }), + ).toBe('3d16dbdf-98c9-59c6-b770-81161144fc78'); + }); +}); diff --git a/packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-application-variable-universal-identifier.util.spec.ts b/packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-application-variable-universal-identifier.util.spec.ts new file mode 100644 index 0000000000..28a5eba3f0 --- /dev/null +++ b/packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-application-variable-universal-identifier.util.spec.ts @@ -0,0 +1,14 @@ +import { getApplicationVariableUniversalIdentifier } from '@/application/deterministic-identifier/get-application-variable-universal-identifier.util'; + +const APP = '11111111-1111-4111-8111-111111111111'; + +describe('getApplicationVariableUniversalIdentifier', () => { + it('derives a deterministic id from the variable key within its application', () => { + expect( + getApplicationVariableUniversalIdentifier({ + applicationUniversalIdentifier: APP, + key: 'API_KEY', + }), + ).toBe('83da1579-61d6-55f5-8b6c-c6ef698608bf'); + }); +}); 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 new file mode 100644 index 0000000000..aed5165eaa --- /dev/null +++ b/packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-command-menu-item-universal-identifier.util.spec.ts @@ -0,0 +1,29 @@ +import { + getCommandMenuItemUniversalIdentifier, + getNavigationCommandUniversalIdentifier, +} 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', () => { + expect( + getCommandMenuItemUniversalIdentifier({ + applicationUniversalIdentifier: APP, + label: 'Create Record', + }), + ).toBe('e284adf7-5332-5253-9c94-9e6b3c57237f'); + }); +}); + +describe('getNavigationCommandUniversalIdentifier', () => { + it('derives a deterministic id from the navigation role within its object', () => { + expect( + getNavigationCommandUniversalIdentifier({ + applicationUniversalIdentifier: APP, + objectUniversalIdentifier: OBJECT, + }), + ).toBe('ec4626f3-3170-5ddc-a309-bdf2a18d4e84'); + }); +}); diff --git a/packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-connection-provider-universal-identifier.util.spec.ts b/packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-connection-provider-universal-identifier.util.spec.ts new file mode 100644 index 0000000000..200f97f81f --- /dev/null +++ b/packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-connection-provider-universal-identifier.util.spec.ts @@ -0,0 +1,14 @@ +import { getConnectionProviderUniversalIdentifier } from '@/application/deterministic-identifier/get-connection-provider-universal-identifier.util'; + +const APP = '11111111-1111-4111-8111-111111111111'; + +describe('getConnectionProviderUniversalIdentifier', () => { + it('derives a deterministic id from the provider name within its application', () => { + expect( + getConnectionProviderUniversalIdentifier({ + applicationUniversalIdentifier: APP, + name: 'Gmail', + }), + ).toBe('69c2371f-465c-57cd-9d36-3fbba8ea8510'); + }); +}); diff --git a/packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-field-permission-universal-identifier.util.spec.ts b/packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-field-permission-universal-identifier.util.spec.ts new file mode 100644 index 0000000000..acea52a40c --- /dev/null +++ b/packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-field-permission-universal-identifier.util.spec.ts @@ -0,0 +1,17 @@ +import { getFieldPermissionUniversalIdentifier } from '@/application/deterministic-identifier/get-field-permission-universal-identifier.util'; + +const APP = '11111111-1111-4111-8111-111111111111'; +const ROLE = '55555555-5555-4555-8555-555555555555'; +const FIELD = '33333333-3333-4333-8333-333333333333'; + +describe('getFieldPermissionUniversalIdentifier', () => { + it('derives a deterministic id from the field within its role', () => { + expect( + getFieldPermissionUniversalIdentifier({ + applicationUniversalIdentifier: APP, + roleUniversalIdentifier: ROLE, + fieldUniversalIdentifier: FIELD, + }), + ).toBe('d94e301d-96c3-559b-a92c-a1f0dbc218e7'); + }); +}); diff --git a/packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-field-universal-identifier.util.spec.ts b/packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-field-universal-identifier.util.spec.ts new file mode 100644 index 0000000000..92cdd170cb --- /dev/null +++ b/packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-field-universal-identifier.util.spec.ts @@ -0,0 +1,16 @@ +import { getFieldUniversalIdentifier } from '@/application/deterministic-identifier/get-field-universal-identifier.util'; + +const APP = '11111111-1111-4111-8111-111111111111'; +const OBJECT = '22222222-2222-4222-8222-222222222222'; + +describe('getFieldUniversalIdentifier', () => { + it('derives a deterministic id from the field name within its object', () => { + expect( + getFieldUniversalIdentifier({ + applicationUniversalIdentifier: APP, + objectUniversalIdentifier: OBJECT, + name: 'createdAt', + }), + ).toBe('b5cc95b6-e78f-531f-b74e-25bf891e8ec7'); + }); +}); diff --git a/packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-front-component-universal-identifier.util.spec.ts b/packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-front-component-universal-identifier.util.spec.ts new file mode 100644 index 0000000000..b939b33443 --- /dev/null +++ b/packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-front-component-universal-identifier.util.spec.ts @@ -0,0 +1,14 @@ +import { getFrontComponentUniversalIdentifier } from '@/application/deterministic-identifier/get-front-component-universal-identifier.util'; + +const APP = '11111111-1111-4111-8111-111111111111'; + +describe('getFrontComponentUniversalIdentifier', () => { + it('derives a deterministic id from the component name within its application', () => { + expect( + getFrontComponentUniversalIdentifier({ + applicationUniversalIdentifier: APP, + componentName: 'HelloWorld', + }), + ).toBe('a81d9442-d9ff-557b-b5b7-494b9c629b23'); + }); +}); diff --git a/packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-index-universal-identifier.util.spec.ts b/packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-index-universal-identifier.util.spec.ts new file mode 100644 index 0000000000..1744d458ae --- /dev/null +++ b/packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-index-universal-identifier.util.spec.ts @@ -0,0 +1,16 @@ +import { getIndexUniversalIdentifier } from '@/application/deterministic-identifier/get-index-universal-identifier.util'; + +const APP = '11111111-1111-4111-8111-111111111111'; +const OBJECT = '22222222-2222-4222-8222-222222222222'; + +describe('getIndexUniversalIdentifier', () => { + it('derives a deterministic id from the generated index name within its object', () => { + expect( + getIndexUniversalIdentifier({ + applicationUniversalIdentifier: APP, + objectUniversalIdentifier: OBJECT, + name: 'IDX_abc', + }), + ).toBe('f3668ea9-452b-514f-905f-07375db25906'); + }); +}); diff --git a/packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-logic-function-universal-identifier.util.spec.ts b/packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-logic-function-universal-identifier.util.spec.ts new file mode 100644 index 0000000000..183bbd62ec --- /dev/null +++ b/packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-logic-function-universal-identifier.util.spec.ts @@ -0,0 +1,14 @@ +import { getLogicFunctionUniversalIdentifier } from '@/application/deterministic-identifier/get-logic-function-universal-identifier.util'; + +const APP = '11111111-1111-4111-8111-111111111111'; + +describe('getLogicFunctionUniversalIdentifier', () => { + it('derives a deterministic id from the function name within its application', () => { + expect( + getLogicFunctionUniversalIdentifier({ + applicationUniversalIdentifier: APP, + name: 'sendEmail', + }), + ).toBe('4a470da0-dd48-5b7f-a1be-4e24fcc5b6c5'); + }); +}); 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 new file mode 100644 index 0000000000..2f87020c8a --- /dev/null +++ b/packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-navigation-menu-item-universal-identifier.util.spec.ts @@ -0,0 +1,14 @@ +import { getNavigationMenuItemUniversalIdentifier } from '@/application/deterministic-identifier/get-navigation-menu-item-universal-identifier.util'; + +const APP = '11111111-1111-4111-8111-111111111111'; + +describe('getNavigationMenuItemUniversalIdentifier', () => { + it('derives a deterministic id from the item name within its application', () => { + expect( + getNavigationMenuItemUniversalIdentifier({ + applicationUniversalIdentifier: APP, + name: 'Home', + }), + ).toBe('dc0ca522-7f09-567d-a7f7-d33824495fc4'); + }); +}); diff --git a/packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-object-permission-universal-identifier.util.spec.ts b/packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-object-permission-universal-identifier.util.spec.ts new file mode 100644 index 0000000000..effbc6dfec --- /dev/null +++ b/packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-object-permission-universal-identifier.util.spec.ts @@ -0,0 +1,17 @@ +import { getObjectPermissionUniversalIdentifier } from '@/application/deterministic-identifier/get-object-permission-universal-identifier.util'; + +const APP = '11111111-1111-4111-8111-111111111111'; +const ROLE = '55555555-5555-4555-8555-555555555555'; +const OBJECT = '22222222-2222-4222-8222-222222222222'; + +describe('getObjectPermissionUniversalIdentifier', () => { + it('derives a deterministic id from the object within its role', () => { + expect( + getObjectPermissionUniversalIdentifier({ + applicationUniversalIdentifier: APP, + roleUniversalIdentifier: ROLE, + objectUniversalIdentifier: OBJECT, + }), + ).toBe('8d78f0a2-6ad6-5ee1-a859-a33bddd42324'); + }); +}); diff --git a/packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-object-universal-identifier.util.spec.ts b/packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-object-universal-identifier.util.spec.ts new file mode 100644 index 0000000000..97021fcb59 --- /dev/null +++ b/packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-object-universal-identifier.util.spec.ts @@ -0,0 +1,14 @@ +import { getObjectUniversalIdentifier } from '@/application/deterministic-identifier/get-object-universal-identifier.util'; + +const APP = '11111111-1111-4111-8111-111111111111'; + +describe('getObjectUniversalIdentifier', () => { + it('derives a deterministic id from the object nameSingular within its application', () => { + expect( + getObjectUniversalIdentifier({ + applicationUniversalIdentifier: APP, + nameSingular: 'rocket', + }), + ).toBe('676819ee-d700-5936-9d6f-58dd7f71ae07'); + }); +}); diff --git a/packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-page-layout-tab-universal-identifier.util.spec.ts b/packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-page-layout-tab-universal-identifier.util.spec.ts new file mode 100644 index 0000000000..db61b0d993 --- /dev/null +++ b/packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-page-layout-tab-universal-identifier.util.spec.ts @@ -0,0 +1,16 @@ +import { getPageLayoutTabUniversalIdentifier } from '@/application/deterministic-identifier/get-page-layout-tab-universal-identifier.util'; + +const APP = '11111111-1111-4111-8111-111111111111'; +const PAGE_LAYOUT = '66666666-6666-4666-8666-666666666666'; + +describe('getPageLayoutTabUniversalIdentifier', () => { + it('derives a deterministic id from the tab title within its page layout', () => { + expect( + getPageLayoutTabUniversalIdentifier({ + applicationUniversalIdentifier: APP, + pageLayoutUniversalIdentifier: PAGE_LAYOUT, + title: 'Home', + }), + ).toBe('f90464e0-2082-540b-8a02-1e59be4cadf4'); + }); +}); diff --git a/packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-page-layout-universal-identifier.util.spec.ts b/packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-page-layout-universal-identifier.util.spec.ts new file mode 100644 index 0000000000..5ce075aa49 --- /dev/null +++ b/packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-page-layout-universal-identifier.util.spec.ts @@ -0,0 +1,39 @@ +import { + getPageLayoutUniversalIdentifier, + getRecordPageLayoutUniversalIdentifier, +} from '@/application/deterministic-identifier/get-page-layout-universal-identifier.util'; + +const APP = '11111111-1111-4111-8111-111111111111'; +const OBJECT = '22222222-2222-4222-8222-222222222222'; + +describe('getPageLayoutUniversalIdentifier', () => { + it('derives a deterministic id from the layout name within its object', () => { + expect( + getPageLayoutUniversalIdentifier({ + applicationUniversalIdentifier: APP, + objectUniversalIdentifier: OBJECT, + name: 'My Dashboard', + }), + ).toBe('dd8a6bdb-4f8e-57c7-8a34-120d5e6430ae'); + }); + + it('derives a deterministic id from the layout name alone when standalone', () => { + expect( + getPageLayoutUniversalIdentifier({ + applicationUniversalIdentifier: APP, + name: 'Standalone', + }), + ).toBe('d382c85c-204a-5648-a101-e65575c3a853'); + }); +}); + +describe('getRecordPageLayoutUniversalIdentifier', () => { + it('derives a deterministic id from the RECORD_PAGE role within its object', () => { + expect( + getRecordPageLayoutUniversalIdentifier({ + applicationUniversalIdentifier: APP, + objectUniversalIdentifier: OBJECT, + }), + ).toBe('80289fa4-d714-5883-bd1d-bc59c7b9a1c2'); + }); +}); diff --git a/packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-page-layout-widget-universal-identifier.util.spec.ts b/packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-page-layout-widget-universal-identifier.util.spec.ts new file mode 100644 index 0000000000..06f6b16216 --- /dev/null +++ b/packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-page-layout-widget-universal-identifier.util.spec.ts @@ -0,0 +1,16 @@ +import { getPageLayoutWidgetUniversalIdentifier } from '@/application/deterministic-identifier/get-page-layout-widget-universal-identifier.util'; + +const APP = '11111111-1111-4111-8111-111111111111'; +const TAB = '77777777-7777-4777-8777-777777777777'; + +describe('getPageLayoutWidgetUniversalIdentifier', () => { + it('derives a deterministic id from the widget title within its tab', () => { + expect( + getPageLayoutWidgetUniversalIdentifier({ + applicationUniversalIdentifier: APP, + pageLayoutTabUniversalIdentifier: TAB, + title: 'Fields', + }), + ).toBe('09cc205a-0cdf-52f0-9917-3ac5a75e69c5'); + }); +}); diff --git a/packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-permission-flag-universal-identifier.util.spec.ts b/packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-permission-flag-universal-identifier.util.spec.ts new file mode 100644 index 0000000000..e29bc29607 --- /dev/null +++ b/packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-permission-flag-universal-identifier.util.spec.ts @@ -0,0 +1,14 @@ +import { getPermissionFlagUniversalIdentifier } from '@/application/deterministic-identifier/get-permission-flag-universal-identifier.util'; + +const APP = '11111111-1111-4111-8111-111111111111'; + +describe('getPermissionFlagUniversalIdentifier', () => { + it('derives a deterministic id from the flag key within its application', () => { + expect( + getPermissionFlagUniversalIdentifier({ + applicationUniversalIdentifier: APP, + key: 'WORKFLOWS', + }), + ).toBe('e9f8bad8-54da-51f7-b7d7-77ed600a589d'); + }); +}); diff --git a/packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-role-permission-flag-universal-identifier.util.spec.ts b/packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-role-permission-flag-universal-identifier.util.spec.ts new file mode 100644 index 0000000000..2bfb622914 --- /dev/null +++ b/packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-role-permission-flag-universal-identifier.util.spec.ts @@ -0,0 +1,17 @@ +import { getRolePermissionFlagUniversalIdentifier } from '@/application/deterministic-identifier/get-role-permission-flag-universal-identifier.util'; + +const APP = '11111111-1111-4111-8111-111111111111'; +const ROLE = '55555555-5555-4555-8555-555555555555'; +const PERMISSION_FLAG = '99999999-9999-4999-8999-999999999999'; + +describe('getRolePermissionFlagUniversalIdentifier', () => { + it('derives a deterministic id from the permission flag within its role', () => { + expect( + getRolePermissionFlagUniversalIdentifier({ + applicationUniversalIdentifier: APP, + roleUniversalIdentifier: ROLE, + permissionFlagUniversalIdentifier: PERMISSION_FLAG, + }), + ).toBe('e1b994dc-25c7-5363-82b3-ac77b697ef6b'); + }); +}); diff --git a/packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-role-universal-identifier.util.spec.ts b/packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-role-universal-identifier.util.spec.ts new file mode 100644 index 0000000000..91622393d4 --- /dev/null +++ b/packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-role-universal-identifier.util.spec.ts @@ -0,0 +1,14 @@ +import { getRoleUniversalIdentifier } from '@/application/deterministic-identifier/get-role-universal-identifier.util'; + +const APP = '11111111-1111-4111-8111-111111111111'; + +describe('getRoleUniversalIdentifier', () => { + it('derives a deterministic id from the role label within its application', () => { + expect( + getRoleUniversalIdentifier({ + applicationUniversalIdentifier: APP, + label: 'Admin', + }), + ).toBe('135023fe-0587-5897-ad22-ce2ee8ab91a0'); + }); +}); diff --git a/packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-select-option-universal-identifier.util.spec.ts b/packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-select-option-universal-identifier.util.spec.ts new file mode 100644 index 0000000000..ec6fad74d0 --- /dev/null +++ b/packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-select-option-universal-identifier.util.spec.ts @@ -0,0 +1,16 @@ +import { getSelectOptionUniversalIdentifier } from '@/application/deterministic-identifier/get-select-option-universal-identifier.util'; + +const APP = '11111111-1111-4111-8111-111111111111'; +const FIELD = '33333333-3333-4333-8333-333333333333'; + +describe('getSelectOptionUniversalIdentifier', () => { + it('derives a deterministic id from the option value within its field', () => { + expect( + getSelectOptionUniversalIdentifier({ + applicationUniversalIdentifier: APP, + fieldUniversalIdentifier: FIELD, + value: 'OPTION_A', + }), + ).toBe('656c6ef0-1b13-5cc0-8c65-99700f364fa9'); + }); +}); diff --git a/packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-skill-universal-identifier.util.spec.ts b/packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-skill-universal-identifier.util.spec.ts new file mode 100644 index 0000000000..7692f8a8b2 --- /dev/null +++ b/packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-skill-universal-identifier.util.spec.ts @@ -0,0 +1,14 @@ +import { getSkillUniversalIdentifier } from '@/application/deterministic-identifier/get-skill-universal-identifier.util'; + +const APP = '11111111-1111-4111-8111-111111111111'; + +describe('getSkillUniversalIdentifier', () => { + it('derives a deterministic id from the skill name within its application', () => { + expect( + getSkillUniversalIdentifier({ + applicationUniversalIdentifier: APP, + name: 'search', + }), + ).toBe('91cd3731-e098-5a62-86af-6fe9ac79812b'); + }); +}); diff --git a/packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-view-field-universal-identifier.util.spec.ts b/packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-view-field-universal-identifier.util.spec.ts new file mode 100644 index 0000000000..0778a1d3ce --- /dev/null +++ b/packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-view-field-universal-identifier.util.spec.ts @@ -0,0 +1,17 @@ +import { getViewFieldUniversalIdentifier } from '@/application/deterministic-identifier/get-view-field-universal-identifier.util'; + +const APP = '11111111-1111-4111-8111-111111111111'; +const VIEW = '44444444-4444-4444-8444-444444444444'; +const FIELD = '33333333-3333-4333-8333-333333333333'; + +describe('getViewFieldUniversalIdentifier', () => { + it('derives a deterministic id from the field it displays within its view', () => { + expect( + getViewFieldUniversalIdentifier({ + applicationUniversalIdentifier: APP, + viewUniversalIdentifier: VIEW, + fieldMetadataUniversalIdentifier: FIELD, + }), + ).toBe('fe533473-ef93-5ed9-b709-b344aa94fb8d'); + }); +}); diff --git a/packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-view-filter-universal-identifier.util.spec.ts b/packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-view-filter-universal-identifier.util.spec.ts new file mode 100644 index 0000000000..12e64bd3de --- /dev/null +++ b/packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-view-filter-universal-identifier.util.spec.ts @@ -0,0 +1,19 @@ +import { getViewFilterUniversalIdentifier } from '@/application/deterministic-identifier/get-view-filter-universal-identifier.util'; + +const APP = '11111111-1111-4111-8111-111111111111'; +const VIEW = '44444444-4444-4444-8444-444444444444'; +const FIELD = '33333333-3333-4333-8333-333333333333'; + +describe('getViewFilterUniversalIdentifier', () => { + it('derives a deterministic id from the field, operand and sub-field within its view', () => { + expect( + getViewFilterUniversalIdentifier({ + applicationUniversalIdentifier: APP, + viewUniversalIdentifier: VIEW, + fieldMetadataUniversalIdentifier: FIELD, + operand: 'is', + subFieldName: null, + }), + ).toBe('d53c51f5-4717-5e81-8ac0-6062b8fdcff6'); + }); +}); diff --git a/packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-view-group-universal-identifier.util.spec.ts b/packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-view-group-universal-identifier.util.spec.ts new file mode 100644 index 0000000000..e1716d70b4 --- /dev/null +++ b/packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-view-group-universal-identifier.util.spec.ts @@ -0,0 +1,16 @@ +import { getViewGroupUniversalIdentifier } from '@/application/deterministic-identifier/get-view-group-universal-identifier.util'; + +const APP = '11111111-1111-4111-8111-111111111111'; +const VIEW = '44444444-4444-4444-8444-444444444444'; + +describe('getViewGroupUniversalIdentifier', () => { + it('derives a deterministic id from the field value it groups by within its view', () => { + expect( + getViewGroupUniversalIdentifier({ + applicationUniversalIdentifier: APP, + viewUniversalIdentifier: VIEW, + fieldValue: 'OPTION_A', + }), + ).toBe('1d0462d5-8fc2-52b0-84f9-b297b785fa4f'); + }); +}); diff --git a/packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-view-sort-universal-identifier.util.spec.ts b/packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-view-sort-universal-identifier.util.spec.ts new file mode 100644 index 0000000000..14562a991a --- /dev/null +++ b/packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-view-sort-universal-identifier.util.spec.ts @@ -0,0 +1,17 @@ +import { getViewSortUniversalIdentifier } from '@/application/deterministic-identifier/get-view-sort-universal-identifier.util'; + +const APP = '11111111-1111-4111-8111-111111111111'; +const VIEW = '44444444-4444-4444-8444-444444444444'; +const FIELD = '33333333-3333-4333-8333-333333333333'; + +describe('getViewSortUniversalIdentifier', () => { + it('derives a deterministic id from the field it sorts within its view', () => { + expect( + getViewSortUniversalIdentifier({ + applicationUniversalIdentifier: APP, + viewUniversalIdentifier: VIEW, + fieldMetadataUniversalIdentifier: FIELD, + }), + ).toBe('e6e591ad-8a29-553b-9a6b-eaaa74667661'); + }); +}); diff --git a/packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-view-universal-identifier.util.spec.ts b/packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-view-universal-identifier.util.spec.ts new file mode 100644 index 0000000000..feadc5d12a --- /dev/null +++ b/packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-view-universal-identifier.util.spec.ts @@ -0,0 +1,43 @@ +import { + getFieldsWidgetViewUniversalIdentifier, + getIndexViewUniversalIdentifier, + getViewUniversalIdentifier, +} from '@/application/deterministic-identifier/get-view-universal-identifier.util'; + +const APP = '11111111-1111-4111-8111-111111111111'; +const OBJECT = '22222222-2222-4222-8222-222222222222'; +const WIDGET = '88888888-8888-4888-8888-888888888888'; + +describe('getViewUniversalIdentifier', () => { + it('derives a deterministic id from the view name within its object', () => { + expect( + getViewUniversalIdentifier({ + applicationUniversalIdentifier: APP, + objectUniversalIdentifier: OBJECT, + name: 'My View', + }), + ).toBe('125140c3-eca9-560d-9020-dad4ac1ebab6'); + }); +}); + +describe('getIndexViewUniversalIdentifier', () => { + it('derives a deterministic id from the stable INDEX view key within its object', () => { + expect( + getIndexViewUniversalIdentifier({ + applicationUniversalIdentifier: APP, + objectUniversalIdentifier: OBJECT, + }), + ).toBe('3803a536-0158-554c-bfb4-e5492323e57f'); + }); +}); + +describe('getFieldsWidgetViewUniversalIdentifier', () => { + it('derives a deterministic id from its 1:1 FIELDS page-layout widget', () => { + expect( + getFieldsWidgetViewUniversalIdentifier({ + applicationUniversalIdentifier: APP, + pageLayoutWidgetUniversalIdentifier: WIDGET, + }), + ).toBe('5f45a1bd-97df-56f7-a8ad-7839ef0045b5'); + }); +}); diff --git a/packages/twenty-shared/src/application/deterministic-identifier/compute-deterministic-uuid.util.ts b/packages/twenty-shared/src/application/deterministic-identifier/compute-deterministic-uuid.util.ts new file mode 100644 index 0000000000..b021ce1dff --- /dev/null +++ b/packages/twenty-shared/src/application/deterministic-identifier/compute-deterministic-uuid.util.ts @@ -0,0 +1,13 @@ +import { v5 } from 'uuid'; + +import { type DeterministicEntityNamespace } from '@/application/deterministic-identifier/deterministic-entity-namespace.type'; + +export const computeDeterministicUuid = ({ + entityNamespace, + value, + applicationUniversalIdentifier, +}: { + entityNamespace: DeterministicEntityNamespace; + value: string; + applicationUniversalIdentifier: string; +}): string => v5(`${entityNamespace}:${value}`, applicationUniversalIdentifier); diff --git a/packages/twenty-shared/src/application/deterministic-identifier/deterministic-entity-namespace.type.ts b/packages/twenty-shared/src/application/deterministic-identifier/deterministic-entity-namespace.type.ts new file mode 100644 index 0000000000..380293bb46 --- /dev/null +++ b/packages/twenty-shared/src/application/deterministic-identifier/deterministic-entity-namespace.type.ts @@ -0,0 +1,4 @@ +import { type AllMetadataName } from '@/metadata/types/all-metadata-name.type'; + +// selectOption is not a syncable metadata entity yet; it will join AllMetadataName once it is. +export type DeterministicEntityNamespace = AllMetadataName | 'selectOption'; diff --git a/packages/twenty-shared/src/application/deterministic-identifier/get-agent-universal-identifier.util.ts b/packages/twenty-shared/src/application/deterministic-identifier/get-agent-universal-identifier.util.ts new file mode 100644 index 0000000000..c7fa256683 --- /dev/null +++ b/packages/twenty-shared/src/application/deterministic-identifier/get-agent-universal-identifier.util.ts @@ -0,0 +1,15 @@ +import { computeDeterministicUuid } from '@/application/deterministic-identifier/compute-deterministic-uuid.util'; + +// An agent is identified by its name within its application. +export const getAgentUniversalIdentifier = ({ + applicationUniversalIdentifier, + name, +}: { + applicationUniversalIdentifier: string; + name: string; +}): string => + computeDeterministicUuid({ + entityNamespace: 'agent', + value: name, + applicationUniversalIdentifier, + }); diff --git a/packages/twenty-shared/src/application/deterministic-identifier/get-application-variable-universal-identifier.util.ts b/packages/twenty-shared/src/application/deterministic-identifier/get-application-variable-universal-identifier.util.ts new file mode 100644 index 0000000000..6257d5bcb4 --- /dev/null +++ b/packages/twenty-shared/src/application/deterministic-identifier/get-application-variable-universal-identifier.util.ts @@ -0,0 +1,15 @@ +import { computeDeterministicUuid } from '@/application/deterministic-identifier/compute-deterministic-uuid.util'; + +// An application variable is identified by its key within its application. +export const getApplicationVariableUniversalIdentifier = ({ + applicationUniversalIdentifier, + key, +}: { + applicationUniversalIdentifier: string; + key: string; +}): string => + computeDeterministicUuid({ + entityNamespace: 'applicationVariable', + value: key, + applicationUniversalIdentifier, + }); 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 new file mode 100644 index 0000000000..5794689ee7 --- /dev/null +++ b/packages/twenty-shared/src/application/deterministic-identifier/get-command-menu-item-universal-identifier.util.ts @@ -0,0 +1,32 @@ +import { computeDeterministicUuid } from '@/application/deterministic-identifier/compute-deterministic-uuid.util'; + +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 = ({ + applicationUniversalIdentifier, + label, +}: { + applicationUniversalIdentifier: string; + label: string; +}): string => + computeDeterministicUuid({ + entityNamespace: 'commandMenuItem', + value: label, + applicationUniversalIdentifier, + }); + +// An object's singleton navigation command, keyed by the object + the 'navigation' role. +export const getNavigationCommandUniversalIdentifier = ({ + applicationUniversalIdentifier, + objectUniversalIdentifier, +}: { + applicationUniversalIdentifier: string; + objectUniversalIdentifier: string; +}): string => + computeDeterministicUuid({ + entityNamespace: 'commandMenuItem', + value: `${objectUniversalIdentifier}:${NAVIGATION_COMMAND_DISCRIMINATOR}`, + applicationUniversalIdentifier, + }); diff --git a/packages/twenty-shared/src/application/deterministic-identifier/get-connection-provider-universal-identifier.util.ts b/packages/twenty-shared/src/application/deterministic-identifier/get-connection-provider-universal-identifier.util.ts new file mode 100644 index 0000000000..206154b354 --- /dev/null +++ b/packages/twenty-shared/src/application/deterministic-identifier/get-connection-provider-universal-identifier.util.ts @@ -0,0 +1,15 @@ +import { computeDeterministicUuid } from '@/application/deterministic-identifier/compute-deterministic-uuid.util'; + +// A connection provider is identified by its name within its application. +export const getConnectionProviderUniversalIdentifier = ({ + applicationUniversalIdentifier, + name, +}: { + applicationUniversalIdentifier: string; + name: string; +}): string => + computeDeterministicUuid({ + entityNamespace: 'connectionProvider', + value: name, + applicationUniversalIdentifier, + }); diff --git a/packages/twenty-shared/src/application/deterministic-identifier/get-field-permission-universal-identifier.util.ts b/packages/twenty-shared/src/application/deterministic-identifier/get-field-permission-universal-identifier.util.ts new file mode 100644 index 0000000000..3b7b9ae2e6 --- /dev/null +++ b/packages/twenty-shared/src/application/deterministic-identifier/get-field-permission-universal-identifier.util.ts @@ -0,0 +1,17 @@ +import { computeDeterministicUuid } from '@/application/deterministic-identifier/compute-deterministic-uuid.util'; + +// A field permission is identified by its field (which implies its object), within its role. +export const getFieldPermissionUniversalIdentifier = ({ + applicationUniversalIdentifier, + roleUniversalIdentifier, + fieldUniversalIdentifier, +}: { + applicationUniversalIdentifier: string; + roleUniversalIdentifier: string; + fieldUniversalIdentifier: string; +}): string => + computeDeterministicUuid({ + entityNamespace: 'fieldPermission', + value: `${roleUniversalIdentifier}:${fieldUniversalIdentifier}`, + applicationUniversalIdentifier, + }); diff --git a/packages/twenty-shared/src/application/deterministic-identifier/get-field-universal-identifier.util.ts b/packages/twenty-shared/src/application/deterministic-identifier/get-field-universal-identifier.util.ts new file mode 100644 index 0000000000..7eeea8098b --- /dev/null +++ b/packages/twenty-shared/src/application/deterministic-identifier/get-field-universal-identifier.util.ts @@ -0,0 +1,17 @@ +import { computeDeterministicUuid } from '@/application/deterministic-identifier/compute-deterministic-uuid.util'; + +// A field is identified by its name within its object. +export const getFieldUniversalIdentifier = ({ + applicationUniversalIdentifier, + objectUniversalIdentifier, + name, +}: { + applicationUniversalIdentifier: string; + objectUniversalIdentifier: string; + name: string; +}): string => + computeDeterministicUuid({ + entityNamespace: 'fieldMetadata', + value: `${objectUniversalIdentifier}:${name}`, + applicationUniversalIdentifier, + }); diff --git a/packages/twenty-shared/src/application/deterministic-identifier/get-front-component-universal-identifier.util.ts b/packages/twenty-shared/src/application/deterministic-identifier/get-front-component-universal-identifier.util.ts new file mode 100644 index 0000000000..02cc2d859b --- /dev/null +++ b/packages/twenty-shared/src/application/deterministic-identifier/get-front-component-universal-identifier.util.ts @@ -0,0 +1,15 @@ +import { computeDeterministicUuid } from '@/application/deterministic-identifier/compute-deterministic-uuid.util'; + +// A front component is identified by its component name within its application. +export const getFrontComponentUniversalIdentifier = ({ + applicationUniversalIdentifier, + componentName, +}: { + applicationUniversalIdentifier: string; + componentName: string; +}): string => + computeDeterministicUuid({ + entityNamespace: 'frontComponent', + value: componentName, + applicationUniversalIdentifier, + }); diff --git a/packages/twenty-shared/src/application/deterministic-identifier/get-index-universal-identifier.util.ts b/packages/twenty-shared/src/application/deterministic-identifier/get-index-universal-identifier.util.ts new file mode 100644 index 0000000000..2e73ce4d96 --- /dev/null +++ b/packages/twenty-shared/src/application/deterministic-identifier/get-index-universal-identifier.util.ts @@ -0,0 +1,18 @@ +import { computeDeterministicUuid } from '@/application/deterministic-identifier/compute-deterministic-uuid.util'; + +// An index is identified by its generated Postgres name (table + ordered columns +// + uniqueness + where-clause); a name change means a different index. +export const getIndexUniversalIdentifier = ({ + applicationUniversalIdentifier, + objectUniversalIdentifier, + name, +}: { + applicationUniversalIdentifier: string; + objectUniversalIdentifier: string; + name: string; +}): string => + computeDeterministicUuid({ + entityNamespace: 'index', + value: `${objectUniversalIdentifier}:${name}`, + applicationUniversalIdentifier, + }); diff --git a/packages/twenty-shared/src/application/deterministic-identifier/get-logic-function-universal-identifier.util.ts b/packages/twenty-shared/src/application/deterministic-identifier/get-logic-function-universal-identifier.util.ts new file mode 100644 index 0000000000..a7e1ccc5c6 --- /dev/null +++ b/packages/twenty-shared/src/application/deterministic-identifier/get-logic-function-universal-identifier.util.ts @@ -0,0 +1,15 @@ +import { computeDeterministicUuid } from '@/application/deterministic-identifier/compute-deterministic-uuid.util'; + +// A logic function is identified by its name within its application. +export const getLogicFunctionUniversalIdentifier = ({ + applicationUniversalIdentifier, + name, +}: { + applicationUniversalIdentifier: string; + name: string; +}): string => + computeDeterministicUuid({ + entityNamespace: 'logicFunction', + value: name, + 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 new file mode 100644 index 0000000000..5f9fe4d2b3 --- /dev/null +++ b/packages/twenty-shared/src/application/deterministic-identifier/get-navigation-menu-item-universal-identifier.util.ts @@ -0,0 +1,15 @@ +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 = ({ + applicationUniversalIdentifier, + name, +}: { + applicationUniversalIdentifier: string; + name: string; +}): string => + computeDeterministicUuid({ + entityNamespace: 'navigationMenuItem', + value: name, + applicationUniversalIdentifier, + }); diff --git a/packages/twenty-shared/src/application/deterministic-identifier/get-object-permission-universal-identifier.util.ts b/packages/twenty-shared/src/application/deterministic-identifier/get-object-permission-universal-identifier.util.ts new file mode 100644 index 0000000000..125ad45c42 --- /dev/null +++ b/packages/twenty-shared/src/application/deterministic-identifier/get-object-permission-universal-identifier.util.ts @@ -0,0 +1,17 @@ +import { computeDeterministicUuid } from '@/application/deterministic-identifier/compute-deterministic-uuid.util'; + +// An object permission is identified by its object, within its role. +export const getObjectPermissionUniversalIdentifier = ({ + applicationUniversalIdentifier, + roleUniversalIdentifier, + objectUniversalIdentifier, +}: { + applicationUniversalIdentifier: string; + roleUniversalIdentifier: string; + objectUniversalIdentifier: string; +}): string => + computeDeterministicUuid({ + entityNamespace: 'objectPermission', + value: `${roleUniversalIdentifier}:${objectUniversalIdentifier}`, + applicationUniversalIdentifier, + }); diff --git a/packages/twenty-shared/src/application/deterministic-identifier/get-object-universal-identifier.util.ts b/packages/twenty-shared/src/application/deterministic-identifier/get-object-universal-identifier.util.ts new file mode 100644 index 0000000000..608fb12dfb --- /dev/null +++ b/packages/twenty-shared/src/application/deterministic-identifier/get-object-universal-identifier.util.ts @@ -0,0 +1,15 @@ +import { computeDeterministicUuid } from '@/application/deterministic-identifier/compute-deterministic-uuid.util'; + +// An object is identified by its nameSingular within its application. +export const getObjectUniversalIdentifier = ({ + applicationUniversalIdentifier, + nameSingular, +}: { + applicationUniversalIdentifier: string; + nameSingular: string; +}): string => + computeDeterministicUuid({ + entityNamespace: 'objectMetadata', + value: nameSingular, + applicationUniversalIdentifier, + }); diff --git a/packages/twenty-shared/src/application/deterministic-identifier/get-page-layout-tab-universal-identifier.util.ts b/packages/twenty-shared/src/application/deterministic-identifier/get-page-layout-tab-universal-identifier.util.ts new file mode 100644 index 0000000000..656758b7fa --- /dev/null +++ b/packages/twenty-shared/src/application/deterministic-identifier/get-page-layout-tab-universal-identifier.util.ts @@ -0,0 +1,17 @@ +import { computeDeterministicUuid } from '@/application/deterministic-identifier/compute-deterministic-uuid.util'; + +// A tab is identified by its title within its page layout. +export const getPageLayoutTabUniversalIdentifier = ({ + applicationUniversalIdentifier, + pageLayoutUniversalIdentifier, + title, +}: { + applicationUniversalIdentifier: string; + pageLayoutUniversalIdentifier: string; + title: string; +}): string => + computeDeterministicUuid({ + entityNamespace: 'pageLayoutTab', + value: `${pageLayoutUniversalIdentifier}:${title}`, + applicationUniversalIdentifier, + }); diff --git a/packages/twenty-shared/src/application/deterministic-identifier/get-page-layout-universal-identifier.util.ts b/packages/twenty-shared/src/application/deterministic-identifier/get-page-layout-universal-identifier.util.ts new file mode 100644 index 0000000000..2eebf635e1 --- /dev/null +++ b/packages/twenty-shared/src/application/deterministic-identifier/get-page-layout-universal-identifier.util.ts @@ -0,0 +1,35 @@ +import { computeDeterministicUuid } from '@/application/deterministic-identifier/compute-deterministic-uuid.util'; + +const RECORD_PAGE_LAYOUT_DISCRIMINATOR = 'RECORD_PAGE'; + +// A page layout is identified by its name within its object (or the app for standalone layouts). +export const getPageLayoutUniversalIdentifier = ({ + applicationUniversalIdentifier, + objectUniversalIdentifier, + name, +}: { + applicationUniversalIdentifier: string; + objectUniversalIdentifier?: string; + name: string; +}): string => + computeDeterministicUuid({ + entityNamespace: 'pageLayout', + value: objectUniversalIdentifier + ? `${objectUniversalIdentifier}:${name}` + : name, + applicationUniversalIdentifier, + }); + +// An object's singleton record page layout (its name is server-generated), keyed by the object + the RECORD_PAGE role. +export const getRecordPageLayoutUniversalIdentifier = ({ + applicationUniversalIdentifier, + objectUniversalIdentifier, +}: { + applicationUniversalIdentifier: string; + objectUniversalIdentifier: string; +}): string => + computeDeterministicUuid({ + entityNamespace: 'pageLayout', + value: `${objectUniversalIdentifier}:${RECORD_PAGE_LAYOUT_DISCRIMINATOR}`, + applicationUniversalIdentifier, + }); diff --git a/packages/twenty-shared/src/application/deterministic-identifier/get-page-layout-widget-universal-identifier.util.ts b/packages/twenty-shared/src/application/deterministic-identifier/get-page-layout-widget-universal-identifier.util.ts new file mode 100644 index 0000000000..330d106890 --- /dev/null +++ b/packages/twenty-shared/src/application/deterministic-identifier/get-page-layout-widget-universal-identifier.util.ts @@ -0,0 +1,17 @@ +import { computeDeterministicUuid } from '@/application/deterministic-identifier/compute-deterministic-uuid.util'; + +// A widget is identified by its title within its tab. +export const getPageLayoutWidgetUniversalIdentifier = ({ + applicationUniversalIdentifier, + pageLayoutTabUniversalIdentifier, + title, +}: { + applicationUniversalIdentifier: string; + pageLayoutTabUniversalIdentifier: string; + title: string; +}): string => + computeDeterministicUuid({ + entityNamespace: 'pageLayoutWidget', + value: `${pageLayoutTabUniversalIdentifier}:${title}`, + applicationUniversalIdentifier, + }); diff --git a/packages/twenty-shared/src/application/deterministic-identifier/get-permission-flag-universal-identifier.util.ts b/packages/twenty-shared/src/application/deterministic-identifier/get-permission-flag-universal-identifier.util.ts new file mode 100644 index 0000000000..94a6829238 --- /dev/null +++ b/packages/twenty-shared/src/application/deterministic-identifier/get-permission-flag-universal-identifier.util.ts @@ -0,0 +1,15 @@ +import { computeDeterministicUuid } from '@/application/deterministic-identifier/compute-deterministic-uuid.util'; + +// A permission flag is identified by its key within its application. +export const getPermissionFlagUniversalIdentifier = ({ + applicationUniversalIdentifier, + key, +}: { + applicationUniversalIdentifier: string; + key: string; +}): string => + computeDeterministicUuid({ + entityNamespace: 'permissionFlag', + value: key, + applicationUniversalIdentifier, + }); diff --git a/packages/twenty-shared/src/application/deterministic-identifier/get-role-permission-flag-universal-identifier.util.ts b/packages/twenty-shared/src/application/deterministic-identifier/get-role-permission-flag-universal-identifier.util.ts new file mode 100644 index 0000000000..fb6927db90 --- /dev/null +++ b/packages/twenty-shared/src/application/deterministic-identifier/get-role-permission-flag-universal-identifier.util.ts @@ -0,0 +1,17 @@ +import { computeDeterministicUuid } from '@/application/deterministic-identifier/compute-deterministic-uuid.util'; + +// A role permission flag is identified by its permission flag, within its role. +export const getRolePermissionFlagUniversalIdentifier = ({ + applicationUniversalIdentifier, + roleUniversalIdentifier, + permissionFlagUniversalIdentifier, +}: { + applicationUniversalIdentifier: string; + roleUniversalIdentifier: string; + permissionFlagUniversalIdentifier: string; +}): string => + computeDeterministicUuid({ + entityNamespace: 'rolePermissionFlag', + value: `${roleUniversalIdentifier}:${permissionFlagUniversalIdentifier}`, + applicationUniversalIdentifier, + }); diff --git a/packages/twenty-shared/src/application/deterministic-identifier/get-role-universal-identifier.util.ts b/packages/twenty-shared/src/application/deterministic-identifier/get-role-universal-identifier.util.ts new file mode 100644 index 0000000000..4365589da5 --- /dev/null +++ b/packages/twenty-shared/src/application/deterministic-identifier/get-role-universal-identifier.util.ts @@ -0,0 +1,15 @@ +import { computeDeterministicUuid } from '@/application/deterministic-identifier/compute-deterministic-uuid.util'; + +// A role is identified by its label within its application. +export const getRoleUniversalIdentifier = ({ + applicationUniversalIdentifier, + label, +}: { + applicationUniversalIdentifier: string; + label: string; +}): string => + computeDeterministicUuid({ + entityNamespace: 'role', + value: label, + applicationUniversalIdentifier, + }); diff --git a/packages/twenty-shared/src/application/deterministic-identifier/get-select-option-universal-identifier.util.ts b/packages/twenty-shared/src/application/deterministic-identifier/get-select-option-universal-identifier.util.ts new file mode 100644 index 0000000000..82591d65b0 --- /dev/null +++ b/packages/twenty-shared/src/application/deterministic-identifier/get-select-option-universal-identifier.util.ts @@ -0,0 +1,17 @@ +import { computeDeterministicUuid } from '@/application/deterministic-identifier/compute-deterministic-uuid.util'; + +// An option is identified by its stable value (not its renameable label) within its field. +export const getSelectOptionUniversalIdentifier = ({ + applicationUniversalIdentifier, + fieldUniversalIdentifier, + value, +}: { + applicationUniversalIdentifier: string; + fieldUniversalIdentifier: string; + value: string; +}): string => + computeDeterministicUuid({ + entityNamespace: 'selectOption', + value: `${fieldUniversalIdentifier}:${value}`, + applicationUniversalIdentifier, + }); diff --git a/packages/twenty-shared/src/application/deterministic-identifier/get-skill-universal-identifier.util.ts b/packages/twenty-shared/src/application/deterministic-identifier/get-skill-universal-identifier.util.ts new file mode 100644 index 0000000000..97bd5cd674 --- /dev/null +++ b/packages/twenty-shared/src/application/deterministic-identifier/get-skill-universal-identifier.util.ts @@ -0,0 +1,15 @@ +import { computeDeterministicUuid } from '@/application/deterministic-identifier/compute-deterministic-uuid.util'; + +// A skill is identified by its name within its application. +export const getSkillUniversalIdentifier = ({ + applicationUniversalIdentifier, + name, +}: { + applicationUniversalIdentifier: string; + name: string; +}): string => + computeDeterministicUuid({ + entityNamespace: 'skill', + value: name, + applicationUniversalIdentifier, + }); diff --git a/packages/twenty-shared/src/application/deterministic-identifier/get-view-field-universal-identifier.util.ts b/packages/twenty-shared/src/application/deterministic-identifier/get-view-field-universal-identifier.util.ts new file mode 100644 index 0000000000..2535a96584 --- /dev/null +++ b/packages/twenty-shared/src/application/deterministic-identifier/get-view-field-universal-identifier.util.ts @@ -0,0 +1,17 @@ +import { computeDeterministicUuid } from '@/application/deterministic-identifier/compute-deterministic-uuid.util'; + +// A view field is identified by the field it displays, within its view. +export const getViewFieldUniversalIdentifier = ({ + applicationUniversalIdentifier, + viewUniversalIdentifier, + fieldMetadataUniversalIdentifier, +}: { + applicationUniversalIdentifier: string; + viewUniversalIdentifier: string; + fieldMetadataUniversalIdentifier: string; +}): string => + computeDeterministicUuid({ + entityNamespace: 'viewField', + value: `${viewUniversalIdentifier}:${fieldMetadataUniversalIdentifier}`, + applicationUniversalIdentifier, + }); diff --git a/packages/twenty-shared/src/application/deterministic-identifier/get-view-filter-universal-identifier.util.ts b/packages/twenty-shared/src/application/deterministic-identifier/get-view-filter-universal-identifier.util.ts new file mode 100644 index 0000000000..e6916e7bfb --- /dev/null +++ b/packages/twenty-shared/src/application/deterministic-identifier/get-view-filter-universal-identifier.util.ts @@ -0,0 +1,21 @@ +import { computeDeterministicUuid } from '@/application/deterministic-identifier/compute-deterministic-uuid.util'; + +// A view filter is identified by its field + operand (+ sub-field), within its view. +export const getViewFilterUniversalIdentifier = ({ + applicationUniversalIdentifier, + viewUniversalIdentifier, + fieldMetadataUniversalIdentifier, + operand, + subFieldName, +}: { + applicationUniversalIdentifier: string; + viewUniversalIdentifier: string; + fieldMetadataUniversalIdentifier: string; + operand: string; + subFieldName?: string | null; +}): string => + computeDeterministicUuid({ + entityNamespace: 'viewFilter', + value: `${viewUniversalIdentifier}:${fieldMetadataUniversalIdentifier}:${operand}:${subFieldName ?? ''}`, + applicationUniversalIdentifier, + }); diff --git a/packages/twenty-shared/src/application/deterministic-identifier/get-view-group-universal-identifier.util.ts b/packages/twenty-shared/src/application/deterministic-identifier/get-view-group-universal-identifier.util.ts new file mode 100644 index 0000000000..470afaab32 --- /dev/null +++ b/packages/twenty-shared/src/application/deterministic-identifier/get-view-group-universal-identifier.util.ts @@ -0,0 +1,17 @@ +import { computeDeterministicUuid } from '@/application/deterministic-identifier/compute-deterministic-uuid.util'; + +// A view group is identified by the field value it groups by, within its view. +export const getViewGroupUniversalIdentifier = ({ + applicationUniversalIdentifier, + viewUniversalIdentifier, + fieldValue, +}: { + applicationUniversalIdentifier: string; + viewUniversalIdentifier: string; + fieldValue: string; +}): string => + computeDeterministicUuid({ + entityNamespace: 'viewGroup', + value: `${viewUniversalIdentifier}:${fieldValue}`, + applicationUniversalIdentifier, + }); diff --git a/packages/twenty-shared/src/application/deterministic-identifier/get-view-sort-universal-identifier.util.ts b/packages/twenty-shared/src/application/deterministic-identifier/get-view-sort-universal-identifier.util.ts new file mode 100644 index 0000000000..5ed7a15592 --- /dev/null +++ b/packages/twenty-shared/src/application/deterministic-identifier/get-view-sort-universal-identifier.util.ts @@ -0,0 +1,17 @@ +import { computeDeterministicUuid } from '@/application/deterministic-identifier/compute-deterministic-uuid.util'; + +// A view sort is identified by the field it sorts, within its view. +export const getViewSortUniversalIdentifier = ({ + applicationUniversalIdentifier, + viewUniversalIdentifier, + fieldMetadataUniversalIdentifier, +}: { + applicationUniversalIdentifier: string; + viewUniversalIdentifier: string; + fieldMetadataUniversalIdentifier: string; +}): string => + computeDeterministicUuid({ + entityNamespace: 'viewSort', + value: `${viewUniversalIdentifier}:${fieldMetadataUniversalIdentifier}`, + applicationUniversalIdentifier, + }); diff --git a/packages/twenty-shared/src/application/deterministic-identifier/get-view-universal-identifier.util.ts b/packages/twenty-shared/src/application/deterministic-identifier/get-view-universal-identifier.util.ts new file mode 100644 index 0000000000..c0f003af22 --- /dev/null +++ b/packages/twenty-shared/src/application/deterministic-identifier/get-view-universal-identifier.util.ts @@ -0,0 +1,48 @@ +import { computeDeterministicUuid } from '@/application/deterministic-identifier/compute-deterministic-uuid.util'; +import { ViewKey } from '@/types/ViewKey'; + +// A view is identified by its name within its object. +export const getViewUniversalIdentifier = ({ + applicationUniversalIdentifier, + objectUniversalIdentifier, + name, +}: { + applicationUniversalIdentifier: string; + objectUniversalIdentifier: string; + name: string; +}): string => + computeDeterministicUuid({ + entityNamespace: 'view', + value: `${objectUniversalIdentifier}:${name}`, + applicationUniversalIdentifier, + }); + +// The INDEX view is an object's singleton main table view; its name is +// server-generated, so it is keyed by the stable INDEX view key. +export const getIndexViewUniversalIdentifier = ({ + applicationUniversalIdentifier, + objectUniversalIdentifier, +}: { + applicationUniversalIdentifier: string; + objectUniversalIdentifier: string; +}): string => + computeDeterministicUuid({ + entityNamespace: 'view', + value: `${objectUniversalIdentifier}:${ViewKey.INDEX}`, + applicationUniversalIdentifier, + }); + +// A fields-widget view is 1:1 with its FIELDS page-layout widget, so it is keyed +// by that widget's deterministic universalIdentifier (its name is server-generated). +export const getFieldsWidgetViewUniversalIdentifier = ({ + applicationUniversalIdentifier, + pageLayoutWidgetUniversalIdentifier, +}: { + applicationUniversalIdentifier: string; + pageLayoutWidgetUniversalIdentifier: string; +}): string => + computeDeterministicUuid({ + entityNamespace: 'view', + value: pageLayoutWidgetUniversalIdentifier, + applicationUniversalIdentifier, + }); diff --git a/packages/twenty-shared/src/application/index.ts b/packages/twenty-shared/src/application/index.ts index bfd7d5b964..ffbf169438 100644 --- a/packages/twenty-shared/src/application/index.ts +++ b/packages/twenty-shared/src/application/index.ts @@ -26,6 +26,43 @@ export { NODE_ESM_CJS_BANNER } from './constants/NodeEsmCjsBanner'; export { OUTPUT_DIR } from './constants/OutputDirectory'; export { TWENTY_STANDARD_APPLICATION_NAME } from './constants/TwentyStandardApplicationName'; export { TWENTY_STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER } from './constants/TwentyStandardApplicationUniversalIdentifier'; +export { computeDeterministicUuid } from './deterministic-identifier/compute-deterministic-uuid.util'; +export type { DeterministicEntityNamespace } from './deterministic-identifier/deterministic-entity-namespace.type'; +export { getAgentUniversalIdentifier } from './deterministic-identifier/get-agent-universal-identifier.util'; +export { getApplicationVariableUniversalIdentifier } from './deterministic-identifier/get-application-variable-universal-identifier.util'; +export { + getCommandMenuItemUniversalIdentifier, + getNavigationCommandUniversalIdentifier, +} from './deterministic-identifier/get-command-menu-item-universal-identifier.util'; +export { getConnectionProviderUniversalIdentifier } from './deterministic-identifier/get-connection-provider-universal-identifier.util'; +export { getFieldPermissionUniversalIdentifier } from './deterministic-identifier/get-field-permission-universal-identifier.util'; +export { getFieldUniversalIdentifier } from './deterministic-identifier/get-field-universal-identifier.util'; +export { 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 { 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'; +export { + getPageLayoutUniversalIdentifier, + getRecordPageLayoutUniversalIdentifier, +} from './deterministic-identifier/get-page-layout-universal-identifier.util'; +export { getPageLayoutWidgetUniversalIdentifier } from './deterministic-identifier/get-page-layout-widget-universal-identifier.util'; +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 { getSelectOptionUniversalIdentifier } from './deterministic-identifier/get-select-option-universal-identifier.util'; +export { getSkillUniversalIdentifier } from './deterministic-identifier/get-skill-universal-identifier.util'; +export { getViewFieldUniversalIdentifier } from './deterministic-identifier/get-view-field-universal-identifier.util'; +export { getViewFilterUniversalIdentifier } from './deterministic-identifier/get-view-filter-universal-identifier.util'; +export { getViewGroupUniversalIdentifier } from './deterministic-identifier/get-view-group-universal-identifier.util'; +export { getViewSortUniversalIdentifier } from './deterministic-identifier/get-view-sort-universal-identifier.util'; +export { + getViewUniversalIdentifier, + getIndexViewUniversalIdentifier, + getFieldsWidgetViewUniversalIdentifier, +} from './deterministic-identifier/get-view-universal-identifier.util'; export { SyncableEntity } from './enums/syncable-entities.enum'; export type { RegularFieldManifest,