Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ export class RefactorNavigationCommandsCommand extends ActiveOrSuspendedWorkspac
workspaceId,
position: nextPosition++,
now,
universalIdentifier,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Navigation-command universalIdentifier uses the old ad-hoc v5 formula instead of the new shared getNavigationCommandUniversalIdentifier helper, producing UIDs inconsistent with the PR's deterministic-ID standard and omitting the owner application UID from the computation.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-1775500013000-refactor-navigation-commands.command.ts, line 205:

<comment>Navigation-command universalIdentifier uses the old ad-hoc v5 formula instead of the new shared `getNavigationCommandUniversalIdentifier` helper, producing UIDs inconsistent with the PR's deterministic-ID standard and omitting the owner application UID from the computation.</comment>

<file context>
@@ -202,6 +202,7 @@ export class RefactorNavigationCommandsCommand extends ActiveOrSuspendedWorkspac
           workspaceId,
           position: nextPosition++,
           now,
+          universalIdentifier,
         }),
       );
</file context>

}),
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ const buildExistingNavigationItem = ({
workspaceId: WORKSPACE_ID,
position,
now: NOW,
universalIdentifier: v5(
objectUniversalIdentifier,
NAVIGATION_COMMAND_UUID_NAMESPACE,
),
});

describe('buildNavigationCommandMenuItemOperationsOrThrow', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export const buildNavigationCommandMenuItemOperationsOrThrow = ({
workspaceId,
position: nextPosition++,
now,
universalIdentifier: commandMenuItemUniversalIdentifier,
}),
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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',
Expand All @@ -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', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -54,6 +53,7 @@ export const buildNavigationFlatCommandMenuItem = ({
workspaceId,
position,
now,
universalIdentifier,
}: {
objectMetadata: {
id: string;
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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 = ({
Comment thread
Weiko marked this conversation as resolved.
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) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Duplicate index-field column resolution logic introduces drift risk between name generation and schema/index execution paths. A future rule change in one place can silently desynchronize deterministic names from actual indexed columns.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/twenty-server/src/engine/metadata-modules/index-metadata/utils/compute-flat-index-name.util.ts, line 37:

<comment>Duplicate index-field column resolution logic introduces drift risk between name generation and schema/index execution paths. A future rule change in one place can silently desynchronize deterministic names from actual indexed columns.</comment>

<file context>
@@ -0,0 +1,101 @@
+}): string => {
+  const orderedIndexColumnNames = [...universalFlatIndexFieldMetadatas]
+    .sort((a, b) => a.order - b.order)
+    .map((flatIndexField) => {
+      const relatedFlatFieldMetadata = objectFlatFieldMetadatas.find(
+        (flatFieldMetadata) =>
</file context>

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,
});
};
Original file line number Diff line number Diff line change
@@ -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,
})),
};
};
Loading
Loading