Compare commits

...

1 Commits

Author SHA1 Message Date
Félix Malfait 86b24382cc chore: remove attachment/note/task morph migration feature flags
- Drop IS_ATTACHMENT_MIGRATED, IS_NOTE_TARGET_MIGRATED, IS_TASK_TARGET_MIGRATED from FeatureFlagKey and defaults
- Frontend always uses morph relation paths for activities and attachments
- Custom object default relations: always morph for attachment, noteTarget, taskTarget (and timelineActivity)
- 1.17 upgrade commands skip when no legacy relation fields remain; no longer set flags
- Sync generated metadata GraphQL types (twenty-front, twenty-sdk)

Made-with: Cursor
2026-03-23 11:22:39 +01:00
20 changed files with 300 additions and 457 deletions
@@ -1700,7 +1700,6 @@ export type FeatureFlag = {
export enum FeatureFlagKey {
IS_AI_ENABLED = 'IS_AI_ENABLED',
IS_APPLICATION_ENABLED = 'IS_APPLICATION_ENABLED',
IS_ATTACHMENT_MIGRATED = 'IS_ATTACHMENT_MIGRATED',
IS_COMMAND_MENU_ITEM_ENABLED = 'IS_COMMAND_MENU_ITEM_ENABLED',
IS_CONNECTED_ACCOUNT_MIGRATED = 'IS_CONNECTED_ACCOUNT_MIGRATED',
IS_DASHBOARD_V2_ENABLED = 'IS_DASHBOARD_V2_ENABLED',
@@ -1713,13 +1712,11 @@ export enum FeatureFlagKey {
IS_MARKETPLACE_ENABLED = 'IS_MARKETPLACE_ENABLED',
IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED = 'IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED',
IS_NAVIGATION_MENU_ITEM_ENABLED = 'IS_NAVIGATION_MENU_ITEM_ENABLED',
IS_NOTE_TARGET_MIGRATED = 'IS_NOTE_TARGET_MIGRATED',
IS_PUBLIC_DOMAIN_ENABLED = 'IS_PUBLIC_DOMAIN_ENABLED',
IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED = 'IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED',
IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED = 'IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED',
IS_RICH_TEXT_V1_MIGRATED = 'IS_RICH_TEXT_V1_MIGRATED',
IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED = 'IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED',
IS_TASK_TARGET_MIGRATED = 'IS_TASK_TARGET_MIGRATED',
IS_UNIQUE_INDEXES_ENABLED = 'IS_UNIQUE_INDEXES_ENABLED',
IS_USAGE_ANALYTICS_ENABLED = 'IS_USAGE_ANALYTICS_ENABLED'
}
@@ -3,17 +3,11 @@ import { type ActivityTargetableObject } from '@/activities/types/ActivityTarget
import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getActivityTargetObjectFieldIdName';
import { CoreObjectNameSingular } from 'twenty-shared/types';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { FeatureFlagKey } from '~/generated-metadata/graphql';
export const useAttachments = (targetableObject: ActivityTargetableObject) => {
const isAttachmentMigrated = useIsFeatureEnabled(
FeatureFlagKey.IS_ATTACHMENT_MIGRATED,
);
const targetableObjectFieldIdName = getActivityTargetObjectFieldIdName({
nameSingular: targetableObject.targetObjectNameSingular,
isMorphRelation: isAttachmentMigrated,
isMorphRelation: true,
});
const { records: attachments, loading } = useFindManyRecords<Attachment>({
@@ -4,12 +4,10 @@ import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getActivi
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { CoreObjectNameSingular } from 'twenty-shared/types';
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useApolloClient, useMutation } from '@apollo/client/react';
import { t } from '@lingui/core/macro';
import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils';
import {
FeatureFlagKey,
FieldMetadataType,
UploadFilesFieldFileDocument,
} from '~/generated-metadata/graphql';
@@ -19,10 +17,6 @@ export const useUploadAttachmentFile = () => {
const [uploadFilesFieldFile] = useMutation(UploadFilesFieldFileDocument, {
client: apolloClient,
});
const isAttachmentMigrated = useIsFeatureEnabled(
FeatureFlagKey.IS_ATTACHMENT_MIGRATED,
);
const { objectMetadataItem: attachmentMetadata } = useObjectMetadataItem({
objectNameSingular: CoreObjectNameSingular.Attachment,
});
@@ -58,7 +52,7 @@ export const useUploadAttachmentFile = () => {
const targetableObjectFieldIdName = getActivityTargetObjectFieldIdName({
nameSingular: targetableObject.targetObjectNameSingular,
isMorphRelation: isAttachmentMigrated,
isMorphRelation: true,
});
const attachmentToCreate = {
@@ -12,9 +12,6 @@ import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMeta
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { FeatureFlagKey } from '~/generated-metadata/graphql';
export const useActivityTargetsForTargetableObjects = ({
objectNameSingular,
@@ -37,22 +34,12 @@ export const useActivityTargetsForTargetableObjects = ({
const objectMetadataItems = useAtomStateValue<EnrichedObjectMetadataItem[]>(
objectMetadataItemsSelector,
);
const isNoteTargetMigrated = useIsFeatureEnabled(
FeatureFlagKey.IS_NOTE_TARGET_MIGRATED,
);
const isTaskTargetMigrated = useIsFeatureEnabled(
FeatureFlagKey.IS_TASK_TARGET_MIGRATED,
);
const isMorphRelation =
objectNameSingular === CoreObjectNameSingular.Task
? isTaskTargetMigrated
: isNoteTargetMigrated;
const activityTargetsFilter = getActivityTargetsFilter({
targetableObjects: targetableObjects,
activityObjectNameSingular: objectNameSingular,
objectMetadataItems,
isMorphRelation,
isMorphRelation: true,
});
const FIND_ACTIVITY_TARGETS_OPERATION_SIGNATURE =
@@ -13,9 +13,7 @@ import { useUpsertFindManyRecordsQueryInCache } from '@/object-record/cache/hook
import { getRecordFromCache } from '@/object-record/cache/utils/getRecordFromCache';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { isDefined } from 'twenty-shared/utils';
import { FeatureFlagKey } from '~/generated-metadata/graphql';
import { sortByAscString } from '~/utils/array/sortByAscString';
export const usePrepareFindManyActivitiesQuery = ({
@@ -35,12 +33,6 @@ export const usePrepareFindManyActivitiesQuery = ({
const cache = useApolloCoreClient().cache;
const { objectMetadataItems } = useObjectMetadataItems();
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const isNoteTargetMigrated = useIsFeatureEnabled(
FeatureFlagKey.IS_NOTE_TARGET_MIGRATED,
);
const isTaskTargetMigrated = useIsFeatureEnabled(
FeatureFlagKey.IS_TASK_TARGET_MIGRATED,
);
const { upsertFindManyRecordsQueryInCache: upsertFindManyActivitiesInCache } =
useUpsertFindManyRecordsQueryInCache({
@@ -124,10 +116,7 @@ export const usePrepareFindManyActivitiesQuery = ({
findActivitiesOperationSignatureFactory({
objectNameSingular: activityObjectNameSingular,
objectMetadataItems,
isMorphRelation:
activityObjectNameSingular === CoreObjectNameSingular.Task
? isTaskTargetMigrated
: isNoteTargetMigrated,
isMorphRelation: true,
});
upsertFindManyActivitiesInCache({
@@ -11,12 +11,10 @@ import { searchRecordStoreFamilyState } from '@/object-record/record-picker/mult
import { type RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { useSetAtomFamilyState } from '@/ui/utilities/state/jotai/hooks/useSetAtomFamilyState';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useCallback } from 'react';
import { useStore } from 'jotai';
import { isDefined } from 'twenty-shared/utils';
import { v4 } from 'uuid';
import { FeatureFlagKey } from '~/generated-metadata/graphql';
type UpdateActivityTargetFromCellProps = {
recordPickerInstanceId: string;
@@ -54,17 +52,6 @@ export const useUpdateActivityTargetFromCell = ({
? activityObjectNameSingular
: joinObjectNameSingular,
});
const isNoteTargetMigrated = useIsFeatureEnabled(
FeatureFlagKey.IS_NOTE_TARGET_MIGRATED,
);
const isTaskTargetMigrated = useIsFeatureEnabled(
FeatureFlagKey.IS_TASK_TARGET_MIGRATED,
);
const isMorphRelation =
activityObjectNameSingular === CoreObjectNameSingular.Task
? isTaskTargetMigrated
: isNoteTargetMigrated;
const store = useStore();
const setRecordStore = useSetAtomFamilyState(
recordStoreFamilyState,
@@ -96,7 +83,7 @@ export const useUpdateActivityTargetFromCell = ({
activityObjectNameSingular,
targetObjectMetadataId: morphItem.objectMetadataId,
objectMetadataItems,
isMorphRelation,
isMorphRelation: true,
});
if (!isDefined(targetFieldName)) {
@@ -211,7 +198,6 @@ export const useUpdateActivityTargetFromCell = ({
activityObjectNameSingular,
createOneActivityTarget,
deleteOneActivityTarget,
isMorphRelation,
setRecordStore,
],
);
@@ -23,7 +23,6 @@ import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePush
import { useRemoveFocusItemFromFocusStackById } from '@/ui/utilities/focus/hooks/useRemoveFocusItemFromFocusStackById';
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { t } from '@lingui/core/macro';
import '@blocknote/core/fonts/inter.css';
import '@blocknote/mantine/style.css';
@@ -32,8 +31,6 @@ import '@blocknote/react/style.css';
import { Key } from 'ts-key-enum';
import { isDefined } from 'twenty-shared/utils';
import { useDebouncedCallback } from 'use-debounce';
import { FeatureFlagKey } from '~/generated-metadata/graphql';
type RichTextFieldEditorProps = {
recordId: string;
objectNameSingular: string;
@@ -82,13 +79,9 @@ export const RichTextFieldEditor = ({
const focusId = `${recordId}-${fieldName}`;
const isAttachmentMigrated = useIsFeatureEnabled(
FeatureFlagKey.IS_ATTACHMENT_MIGRATED,
);
const attachmentTargetFieldIdName = getActivityTargetObjectFieldIdName({
nameSingular: objectNameSingular,
isMorphRelation: isAttachmentMigrated,
isMorphRelation: true,
});
const { records: attachments } = useFindManyRecords<Attachment>({
@@ -11,11 +11,9 @@ import { StandaloneRichTextEditorContent } from '@/page-layout/widgets/standalon
import { useLayoutRenderingContext } from '@/ui/layout/contexts/LayoutRenderingContext';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { styled } from '@linaria/react';
import { isDefined } from 'twenty-shared/utils';
import {
FeatureFlagKey,
PageLayoutType,
type StandaloneRichTextConfiguration,
} from '~/generated-metadata/graphql';
@@ -47,15 +45,12 @@ export const StandaloneRichTextWidget = ({
);
const { targetRecordIdentifier, layoutType } = useLayoutRenderingContext();
const isAttachmentMigrated = useIsFeatureEnabled(
FeatureFlagKey.IS_ATTACHMENT_MIGRATED,
);
const isDashboard = layoutType === PageLayoutType.DASHBOARD;
const dashboardId = isDashboard ? targetRecordIdentifier?.id : undefined;
const attachmentTargetFieldIdName = getActivityTargetObjectFieldIdName({
nameSingular: CoreObjectNameSingular.Dashboard,
isMorphRelation: isAttachmentMigrated,
isMorphRelation: true,
});
const configuration = widget.configuration as
@@ -1544,9 +1544,6 @@ enum FeatureFlagKey {
IS_PUBLIC_DOMAIN_ENABLED
IS_EMAILING_DOMAIN_ENABLED
IS_DASHBOARD_V2_ENABLED
IS_ATTACHMENT_MIGRATED
IS_NOTE_TARGET_MIGRATED
IS_TASK_TARGET_MIGRATED
IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED
IS_JUNCTION_RELATIONS_ENABLED
IS_COMMAND_MENU_ITEM_ENABLED
@@ -1256,7 +1256,7 @@ export interface FeatureFlag {
__typename: 'FeatureFlag'
}
export type FeatureFlagKey = 'IS_UNIQUE_INDEXES_ENABLED' | 'IS_JSON_FILTER_ENABLED' | 'IS_AI_ENABLED' | 'IS_APPLICATION_ENABLED' | 'IS_MARKETPLACE_ENABLED' | 'IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED' | 'IS_PUBLIC_DOMAIN_ENABLED' | 'IS_EMAILING_DOMAIN_ENABLED' | 'IS_DASHBOARD_V2_ENABLED' | 'IS_ATTACHMENT_MIGRATED' | 'IS_NOTE_TARGET_MIGRATED' | 'IS_TASK_TARGET_MIGRATED' | 'IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED' | 'IS_JUNCTION_RELATIONS_ENABLED' | 'IS_COMMAND_MENU_ITEM_ENABLED' | 'IS_NAVIGATION_MENU_ITEM_ENABLED' | 'IS_DATE_TIME_WHOLE_DAY_FILTER_ENABLED' | 'IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED' | 'IS_DRAFT_EMAIL_ENABLED' | 'IS_USAGE_ANALYTICS_ENABLED' | 'IS_RICH_TEXT_V1_MIGRATED' | 'IS_DIRECT_GRAPHQL_EXECUTION_ENABLED' | 'IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED' | 'IS_CONNECTED_ACCOUNT_MIGRATED'
export type FeatureFlagKey = 'IS_UNIQUE_INDEXES_ENABLED' | 'IS_JSON_FILTER_ENABLED' | 'IS_AI_ENABLED' | 'IS_APPLICATION_ENABLED' | 'IS_MARKETPLACE_ENABLED' | 'IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED' | 'IS_PUBLIC_DOMAIN_ENABLED' | 'IS_EMAILING_DOMAIN_ENABLED' | 'IS_DASHBOARD_V2_ENABLED' | 'IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED' | 'IS_JUNCTION_RELATIONS_ENABLED' | 'IS_COMMAND_MENU_ITEM_ENABLED' | 'IS_NAVIGATION_MENU_ITEM_ENABLED' | 'IS_DATE_TIME_WHOLE_DAY_FILTER_ENABLED' | 'IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED' | 'IS_DRAFT_EMAIL_ENABLED' | 'IS_USAGE_ANALYTICS_ENABLED' | 'IS_RICH_TEXT_V1_MIGRATED' | 'IS_DIRECT_GRAPHQL_EXECUTION_ENABLED' | 'IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED' | 'IS_CONNECTED_ACCOUNT_MIGRATED'
export interface SSOIdentityProvider {
id: Scalars['UUID']
@@ -9067,9 +9067,6 @@ export const enumFeatureFlagKey = {
IS_PUBLIC_DOMAIN_ENABLED: 'IS_PUBLIC_DOMAIN_ENABLED' as const,
IS_EMAILING_DOMAIN_ENABLED: 'IS_EMAILING_DOMAIN_ENABLED' as const,
IS_DASHBOARD_V2_ENABLED: 'IS_DASHBOARD_V2_ENABLED' as const,
IS_ATTACHMENT_MIGRATED: 'IS_ATTACHMENT_MIGRATED' as const,
IS_NOTE_TARGET_MIGRATED: 'IS_NOTE_TARGET_MIGRATED' as const,
IS_TASK_TARGET_MIGRATED: 'IS_TASK_TARGET_MIGRATED' as const,
IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED: 'IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED' as const,
IS_JUNCTION_RELATIONS_ENABLED: 'IS_JUNCTION_RELATIONS_ENABLED' as const,
IS_COMMAND_MENU_ITEM_ENABLED: 'IS_COMMAND_MENU_ITEM_ENABLED' as const,
@@ -3,7 +3,6 @@ import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
import {
FeatureFlagKey,
FieldMetadataType,
type FieldMetadataSettings,
} from 'twenty-shared/types';
@@ -14,7 +13,6 @@ import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
import { getFlatFieldsFromFlatObjectMetadata } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-flat-fields-for-flat-object-metadata.util';
import { type FlatApplicationCacheMaps } from 'src/engine/core-modules/application/types/flat-application-cache-maps.type';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util';
@@ -39,13 +37,12 @@ type RelationFieldMetadataSettings =
@Command({
name: 'upgrade:1-17:migrate-attachment-to-morph-relations',
description:
'Migrate attachment relations to morph relation fields and set feature flag',
'Migrate attachment relations to morph relation fields (idempotent)',
})
export class MigrateAttachmentToMorphRelationsCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
constructor(
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
private readonly featureFlagService: FeatureFlagService,
@InjectDataSource()
private readonly coreDataSource: DataSource,
private readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
@@ -61,19 +58,8 @@ export class MigrateAttachmentToMorphRelationsCommand extends ActiveOrSuspendedW
workspaceId,
options,
}: RunOnWorkspaceArgs): Promise<void> {
const isMigrated = await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IS_ATTACHMENT_MIGRATED,
workspaceId,
);
this.logger.log(`Migrating attachments for workspace ${workspaceId}`);
if (isMigrated) {
this.logger.log(`Attachment migration already completed. Skipping...`);
return;
}
if (options.dryRun) {
this.logger.log(
`Would have migrated attachments for workspace ${workspaceId}. Skipping...`,
@@ -82,101 +68,107 @@ export class MigrateAttachmentToMorphRelationsCommand extends ActiveOrSuspendedW
return;
}
const schemaName = getWorkspaceSchemaName(workspaceId);
const tableName = 'attachment';
const {
flatObjectMetadataMaps,
flatFieldMetadataMaps,
flatApplicationMaps,
} = await this.workspaceCacheService.getOrRecompute(workspaceId, [
'flatObjectMetadataMaps',
'flatFieldMetadataMaps',
'flatApplicationMaps',
]);
const attachmentTargetFieldUniversalIdentifiers = new Set<string>([
STANDARD_OBJECTS.attachment.fields.targetTask.universalIdentifier,
STANDARD_OBJECTS.attachment.fields.targetNote.universalIdentifier,
STANDARD_OBJECTS.attachment.fields.targetPerson.universalIdentifier,
STANDARD_OBJECTS.attachment.fields.targetCompany.universalIdentifier,
STANDARD_OBJECTS.attachment.fields.targetOpportunity.universalIdentifier,
STANDARD_OBJECTS.attachment.fields.targetDashboard.universalIdentifier,
STANDARD_OBJECTS.attachment.fields.targetWorkflow.universalIdentifier,
]);
const attachmentObjectMetadata =
findFlatEntityByUniversalIdentifier<FlatObjectMetadata>({
flatEntityMaps: flatObjectMetadataMaps,
universalIdentifier: STANDARD_OBJECTS.attachment.universalIdentifier,
});
if (!attachmentObjectMetadata) {
this.logger.error(
`🟥 Attachment object metadata not found for workspace ${workspaceId}`,
);
return;
}
const attachmentFieldMetadatas = getFlatFieldsFromFlatObjectMetadata(
attachmentObjectMetadata,
flatFieldMetadataMaps,
);
const attachmentTargetRelationFields = attachmentFieldMetadatas
.filter(isMorphOrRelationFlatFieldMetadata)
.filter(
(field): field is FlatFieldMetadata<FieldMetadataType.RELATION> =>
field.type === FieldMetadataType.RELATION,
)
.filter((field) => {
const isStandardAppField = this.isTwentyStandardApplicationField({
field,
flatApplicationMaps,
workspaceId,
});
const isStandardTarget =
isStandardAppField &&
attachmentTargetFieldUniversalIdentifiers.has(
field.universalIdentifier,
);
const targetObjectMetadata = findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: field.relationTargetObjectMetadataId,
flatEntityMaps: flatObjectMetadataMaps,
});
const isCustomTarget =
!isStandardAppField && targetObjectMetadata?.isCustom === true;
return isStandardTarget || isCustomTarget;
});
const fieldMigrations = attachmentTargetRelationFields.map((field) => {
const newFieldName = `target${capitalize(field.name)}`;
const relationSettings: RelationFieldMetadataSettings = field.settings;
const oldJoinColumnName =
relationSettings?.joinColumnName ??
computeMorphOrRelationFieldJoinColumnName({ name: field.name });
const newJoinColumnName = computeMorphOrRelationFieldJoinColumnName({
name: newFieldName,
});
return {
field,
newFieldName,
oldJoinColumnName,
newJoinColumnName,
};
});
if (fieldMigrations.length === 0) {
this.logger.log(
`Attachment morph migration already applied or nothing to migrate. Skipping workspace ${workspaceId}...`,
);
return;
}
const queryRunner = this.coreDataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
const schemaName = getWorkspaceSchemaName(workspaceId);
const tableName = 'attachment';
const {
flatObjectMetadataMaps,
flatFieldMetadataMaps,
flatApplicationMaps,
} = await this.workspaceCacheService.getOrRecompute(workspaceId, [
'flatObjectMetadataMaps',
'flatFieldMetadataMaps',
'flatApplicationMaps',
]);
const attachmentTargetFieldUniversalIdentifiers = new Set<string>([
STANDARD_OBJECTS.attachment.fields.targetTask.universalIdentifier,
STANDARD_OBJECTS.attachment.fields.targetNote.universalIdentifier,
STANDARD_OBJECTS.attachment.fields.targetPerson.universalIdentifier,
STANDARD_OBJECTS.attachment.fields.targetCompany.universalIdentifier,
STANDARD_OBJECTS.attachment.fields.targetOpportunity
.universalIdentifier,
STANDARD_OBJECTS.attachment.fields.targetDashboard.universalIdentifier,
STANDARD_OBJECTS.attachment.fields.targetWorkflow.universalIdentifier,
]);
const attachmentObjectMetadata =
findFlatEntityByUniversalIdentifier<FlatObjectMetadata>({
flatEntityMaps: flatObjectMetadataMaps,
universalIdentifier: STANDARD_OBJECTS.attachment.universalIdentifier,
});
if (!attachmentObjectMetadata) {
this.logger.error(
`🟥 Attachment object metadata not found for workspace ${workspaceId}`,
);
return;
}
const attachmentFieldMetadatas = getFlatFieldsFromFlatObjectMetadata(
attachmentObjectMetadata,
flatFieldMetadataMaps,
);
const attachmentTargetRelationFields = attachmentFieldMetadatas
.filter(isMorphOrRelationFlatFieldMetadata)
.filter(
(field): field is FlatFieldMetadata<FieldMetadataType.RELATION> =>
field.type === FieldMetadataType.RELATION,
)
.filter((field) => {
const isStandardAppField = this.isTwentyStandardApplicationField({
field,
flatApplicationMaps,
workspaceId,
});
const isStandardTarget =
isStandardAppField &&
attachmentTargetFieldUniversalIdentifiers.has(
field.universalIdentifier,
);
const targetObjectMetadata =
findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: field.relationTargetObjectMetadataId,
flatEntityMaps: flatObjectMetadataMaps,
});
const isCustomTarget =
!isStandardAppField && targetObjectMetadata?.isCustom === true;
return isStandardTarget || isCustomTarget;
});
const fieldMigrations = attachmentTargetRelationFields.map((field) => {
const newFieldName = `target${capitalize(field.name)}`;
const relationSettings: RelationFieldMetadataSettings = field.settings;
const oldJoinColumnName =
relationSettings?.joinColumnName ??
computeMorphOrRelationFieldJoinColumnName({ name: field.name });
const newJoinColumnName = computeMorphOrRelationFieldJoinColumnName({
name: newFieldName,
});
return {
field,
newFieldName,
oldJoinColumnName,
newJoinColumnName,
};
});
for (const { oldJoinColumnName, newJoinColumnName } of fieldMigrations) {
if (oldJoinColumnName === newJoinColumnName) {
this.logger.log(
@@ -253,11 +245,6 @@ export class MigrateAttachmentToMorphRelationsCommand extends ActiveOrSuspendedW
await queryRunner.commitTransaction();
await this.featureFlagService.enableFeatureFlags(
[FeatureFlagKey.IS_ATTACHMENT_MIGRATED],
workspaceId,
);
const relatedMetadataNames =
getMetadataRelatedMetadataNames('fieldMetadata');
const relatedCacheKeysToInvalidate: WorkspaceCacheKeyName[] =
@@ -265,7 +252,6 @@ export class MigrateAttachmentToMorphRelationsCommand extends ActiveOrSuspendedW
const cacheKeysToInvalidate: WorkspaceCacheKeyName[] = [
'flatFieldMetadataMaps',
...relatedCacheKeysToInvalidate,
'featureFlagsMap',
];
this.logger.log(
@@ -282,10 +268,6 @@ export class MigrateAttachmentToMorphRelationsCommand extends ActiveOrSuspendedW
);
this.logger.log(`Cache flushed`);
this.logger.log(
`Set IS_ATTACHMENT_MIGRATED feature flag for workspace ${workspaceId}`,
);
this.logger.log(`Flush cache for workspace ${workspaceId}`);
await this.workspaceCacheStorageService.flush(workspaceId);
} catch (error) {
@@ -3,7 +3,6 @@ import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
import {
FeatureFlagKey,
FieldMetadataType,
type FieldMetadataSettings,
} from 'twenty-shared/types';
@@ -14,7 +13,6 @@ import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
import { getFlatFieldsFromFlatObjectMetadata } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-flat-fields-for-flat-object-metadata.util';
import { type FlatApplicationCacheMaps } from 'src/engine/core-modules/application/types/flat-application-cache-maps.type';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util';
@@ -38,13 +36,12 @@ type RelationFieldMetadataSettings =
@Command({
name: 'upgrade:1-17:migrate-note-target-to-morph-relations',
description:
'Migrate noteTarget relations to morph relation fields and set feature flag',
'Migrate noteTarget relations to morph relation fields (idempotent)',
})
export class MigrateNoteTargetToMorphRelationsCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
constructor(
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
private readonly featureFlagService: FeatureFlagService,
@InjectDataSource()
private readonly coreDataSource: DataSource,
private readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
@@ -60,19 +57,8 @@ export class MigrateNoteTargetToMorphRelationsCommand extends ActiveOrSuspendedW
workspaceId,
options,
}: RunOnWorkspaceArgs): Promise<void> {
const isMigrated = await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IS_NOTE_TARGET_MIGRATED,
workspaceId,
);
this.logger.log(`Migrating noteTarget for workspace ${workspaceId}`);
if (isMigrated) {
this.logger.log(`NoteTarget migration already completed. Skipping...`);
return;
}
if (options.dryRun) {
this.logger.log(
`Would have migrated noteTarget for workspace ${workspaceId}. Skipping...`,
@@ -81,95 +67,102 @@ export class MigrateNoteTargetToMorphRelationsCommand extends ActiveOrSuspendedW
return;
}
const schemaName = getWorkspaceSchemaName(workspaceId);
const tableName = 'noteTarget';
const {
flatObjectMetadataMaps,
flatFieldMetadataMaps,
flatApplicationMaps,
} = await this.workspaceCacheService.getOrRecompute(workspaceId, [
'flatObjectMetadataMaps',
'flatFieldMetadataMaps',
'flatApplicationMaps',
]);
const noteTargetFieldUniversalIdentifiers = new Set<string>([
STANDARD_OBJECTS.noteTarget.fields.targetPerson.universalIdentifier,
STANDARD_OBJECTS.noteTarget.fields.targetCompany.universalIdentifier,
STANDARD_OBJECTS.noteTarget.fields.targetOpportunity.universalIdentifier,
]);
const noteTargetObjectMetadata =
findFlatEntityByUniversalIdentifier<FlatObjectMetadata>({
flatEntityMaps: flatObjectMetadataMaps,
universalIdentifier: STANDARD_OBJECTS.noteTarget.universalIdentifier,
});
if (!noteTargetObjectMetadata) {
this.logger.error(
`🟥 ${tableName} object metadata not found for workspace ${workspaceId}`,
);
return;
}
const noteTargetFieldMetadatas = getFlatFieldsFromFlatObjectMetadata(
noteTargetObjectMetadata,
flatFieldMetadataMaps,
);
const noteTargetRelationFields = noteTargetFieldMetadatas
.filter(isMorphOrRelationFlatFieldMetadata)
.filter((field) => field.type === FieldMetadataType.RELATION)
.filter((field) => {
const isStandardAppField = this.isTwentyStandardApplicationField({
field,
flatApplicationMaps,
workspaceId,
});
const isStandardTarget =
isStandardAppField &&
noteTargetFieldUniversalIdentifiers.has(field.universalIdentifier);
const targetObjectMetadata = field.relationTargetObjectMetadataId
? findFlatEntityByIdInFlatEntityMaps({
flatEntityMaps: flatObjectMetadataMaps,
flatEntityId: field.relationTargetObjectMetadataId,
})
: undefined;
const isCustomTarget =
!isStandardAppField && targetObjectMetadata?.isCustom === true;
return isStandardTarget || isCustomTarget;
});
const fieldMigrations = noteTargetRelationFields.map((field) => {
const newFieldName = `target${capitalize(field.name)}`;
const newFieldLabel = 'Target';
const relationSettings: RelationFieldMetadataSettings = field.settings;
const oldJoinColumnName =
relationSettings?.joinColumnName ??
computeMorphOrRelationFieldJoinColumnName({ name: field.name });
const newJoinColumnName = computeMorphOrRelationFieldJoinColumnName({
name: newFieldName,
});
return {
field,
newFieldName,
newFieldLabel,
oldJoinColumnName,
newJoinColumnName,
};
});
if (fieldMigrations.length === 0) {
this.logger.log(
`NoteTarget morph migration already applied or nothing to migrate. Skipping workspace ${workspaceId}...`,
);
return;
}
const queryRunner = this.coreDataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
const schemaName = getWorkspaceSchemaName(workspaceId);
const tableName = 'noteTarget';
const {
flatObjectMetadataMaps,
flatFieldMetadataMaps,
flatApplicationMaps,
} = await this.workspaceCacheService.getOrRecompute(workspaceId, [
'flatObjectMetadataMaps',
'flatFieldMetadataMaps',
'flatApplicationMaps',
]);
const noteTargetFieldUniversalIdentifiers = new Set<string>([
STANDARD_OBJECTS.noteTarget.fields.targetPerson.universalIdentifier,
STANDARD_OBJECTS.noteTarget.fields.targetCompany.universalIdentifier,
STANDARD_OBJECTS.noteTarget.fields.targetOpportunity
.universalIdentifier,
]);
const noteTargetObjectMetadata =
findFlatEntityByUniversalIdentifier<FlatObjectMetadata>({
flatEntityMaps: flatObjectMetadataMaps,
universalIdentifier: STANDARD_OBJECTS.noteTarget.universalIdentifier,
});
if (!noteTargetObjectMetadata) {
this.logger.error(
`🟥 ${tableName} object metadata not found for workspace ${workspaceId}`,
);
return;
}
const noteTargetFieldMetadatas = getFlatFieldsFromFlatObjectMetadata(
noteTargetObjectMetadata,
flatFieldMetadataMaps,
);
const noteTargetRelationFields = noteTargetFieldMetadatas
.filter(isMorphOrRelationFlatFieldMetadata)
.filter((field) => field.type === FieldMetadataType.RELATION)
.filter((field) => {
const isStandardAppField = this.isTwentyStandardApplicationField({
field,
flatApplicationMaps,
workspaceId,
});
const isStandardTarget =
isStandardAppField &&
noteTargetFieldUniversalIdentifiers.has(field.universalIdentifier);
const targetObjectMetadata = field.relationTargetObjectMetadataId
? findFlatEntityByIdInFlatEntityMaps({
flatEntityMaps: flatObjectMetadataMaps,
flatEntityId: field.relationTargetObjectMetadataId,
})
: undefined;
const isCustomTarget =
!isStandardAppField && targetObjectMetadata?.isCustom === true;
return isStandardTarget || isCustomTarget;
});
const fieldMigrations = noteTargetRelationFields.map((field) => {
const newFieldName = `target${capitalize(field.name)}`;
const newFieldLabel = 'Target';
const relationSettings: RelationFieldMetadataSettings = field.settings;
const oldJoinColumnName =
relationSettings?.joinColumnName ??
computeMorphOrRelationFieldJoinColumnName({ name: field.name });
const newJoinColumnName = computeMorphOrRelationFieldJoinColumnName({
name: newFieldName,
});
return {
field,
newFieldName,
newFieldLabel,
oldJoinColumnName,
newJoinColumnName,
};
});
// Rename columns
for (const { oldJoinColumnName, newJoinColumnName } of fieldMigrations) {
if (oldJoinColumnName === newJoinColumnName) {
@@ -250,11 +243,6 @@ export class MigrateNoteTargetToMorphRelationsCommand extends ActiveOrSuspendedW
await queryRunner.commitTransaction();
await this.featureFlagService.enableFeatureFlags(
[FeatureFlagKey.IS_NOTE_TARGET_MIGRATED],
workspaceId,
);
const relatedMetadataNames =
getMetadataRelatedMetadataNames('fieldMetadata');
const relatedCacheKeysToInvalidate: WorkspaceCacheKeyName[] =
@@ -262,7 +250,6 @@ export class MigrateNoteTargetToMorphRelationsCommand extends ActiveOrSuspendedW
const cacheKeysToInvalidate: WorkspaceCacheKeyName[] = [
'flatFieldMetadataMaps',
...relatedCacheKeysToInvalidate,
'featureFlagsMap',
];
this.logger.log(
@@ -279,10 +266,6 @@ export class MigrateNoteTargetToMorphRelationsCommand extends ActiveOrSuspendedW
);
this.logger.log(`Cache flushed`);
this.logger.log(
`Set IS_NOTE_TARGET_MIGRATED feature flag for workspace ${workspaceId}`,
);
this.logger.log(`Flush cache for workspace ${workspaceId}`);
await this.workspaceCacheStorageService.flush(workspaceId);
} catch (error) {
@@ -3,7 +3,6 @@ import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
import {
FeatureFlagKey,
FieldMetadataType,
type FieldMetadataSettings,
} from 'twenty-shared/types';
@@ -14,7 +13,6 @@ import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
import { getFlatFieldsFromFlatObjectMetadata } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-flat-fields-for-flat-object-metadata.util';
import { type FlatApplicationCacheMaps } from 'src/engine/core-modules/application/types/flat-application-cache-maps.type';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util';
@@ -38,13 +36,12 @@ type RelationFieldMetadataSettings =
@Command({
name: 'upgrade:1-17:migrate-task-target-to-morph-relations',
description:
'Migrate taskTarget relations to morph relation fields and set feature flag',
'Migrate taskTarget relations to morph relation fields (idempotent)',
})
export class MigrateTaskTargetToMorphRelationsCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
constructor(
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
private readonly featureFlagService: FeatureFlagService,
@InjectDataSource()
private readonly coreDataSource: DataSource,
private readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
@@ -60,19 +57,8 @@ export class MigrateTaskTargetToMorphRelationsCommand extends ActiveOrSuspendedW
workspaceId,
options,
}: RunOnWorkspaceArgs): Promise<void> {
const isMigrated = await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IS_TASK_TARGET_MIGRATED,
workspaceId,
);
this.logger.log(`Migrating taskTarget for workspace ${workspaceId}`);
if (isMigrated) {
this.logger.log(`TaskTarget migration already completed. Skipping...`);
return;
}
if (options.dryRun) {
this.logger.log(
`Would have migrated taskTarget for workspace ${workspaceId}. Skipping...`,
@@ -81,95 +67,102 @@ export class MigrateTaskTargetToMorphRelationsCommand extends ActiveOrSuspendedW
return;
}
const schemaName = getWorkspaceSchemaName(workspaceId);
const tableName = 'taskTarget';
const {
flatObjectMetadataMaps,
flatFieldMetadataMaps,
flatApplicationMaps,
} = await this.workspaceCacheService.getOrRecompute(workspaceId, [
'flatObjectMetadataMaps',
'flatFieldMetadataMaps',
'flatApplicationMaps',
]);
const taskTargetFieldUniversalIdentifiers = new Set<string>([
STANDARD_OBJECTS.taskTarget.fields.targetPerson.universalIdentifier,
STANDARD_OBJECTS.taskTarget.fields.targetCompany.universalIdentifier,
STANDARD_OBJECTS.taskTarget.fields.targetOpportunity.universalIdentifier,
]);
const taskTargetObjectMetadata =
findFlatEntityByUniversalIdentifier<FlatObjectMetadata>({
flatEntityMaps: flatObjectMetadataMaps,
universalIdentifier: STANDARD_OBJECTS.taskTarget.universalIdentifier,
});
if (!taskTargetObjectMetadata) {
this.logger.error(
`🟥 ${tableName} object metadata not found for workspace ${workspaceId}`,
);
return;
}
const taskTargetFieldMetadatas = getFlatFieldsFromFlatObjectMetadata(
taskTargetObjectMetadata,
flatFieldMetadataMaps,
);
const taskTargetRelationFields = taskTargetFieldMetadatas
.filter(isMorphOrRelationFlatFieldMetadata)
.filter((field) => field.type === FieldMetadataType.RELATION)
.filter((field) => {
const isStandardAppField = this.isTwentyStandardApplicationField({
field,
flatApplicationMaps,
workspaceId,
});
const isStandardTarget =
isStandardAppField &&
taskTargetFieldUniversalIdentifiers.has(field.universalIdentifier);
const targetObjectMetadata = field.relationTargetObjectMetadataId
? findFlatEntityByIdInFlatEntityMaps({
flatEntityMaps: flatObjectMetadataMaps,
flatEntityId: field.relationTargetObjectMetadataId,
})
: undefined;
const isCustomTarget =
!isStandardAppField && targetObjectMetadata?.isCustom === true;
return isStandardTarget || isCustomTarget;
});
const fieldMigrations = taskTargetRelationFields.map((field) => {
const newFieldName = `target${capitalize(field.name)}`;
const newFieldLabel = 'Target';
const relationSettings: RelationFieldMetadataSettings = field.settings;
const oldJoinColumnName =
relationSettings?.joinColumnName ??
computeMorphOrRelationFieldJoinColumnName({ name: field.name });
const newJoinColumnName = computeMorphOrRelationFieldJoinColumnName({
name: newFieldName,
});
return {
field,
newFieldName,
newFieldLabel,
oldJoinColumnName,
newJoinColumnName,
};
});
if (fieldMigrations.length === 0) {
this.logger.log(
`TaskTarget morph migration already applied or nothing to migrate. Skipping workspace ${workspaceId}...`,
);
return;
}
const queryRunner = this.coreDataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
const schemaName = getWorkspaceSchemaName(workspaceId);
const tableName = 'taskTarget';
const {
flatObjectMetadataMaps,
flatFieldMetadataMaps,
flatApplicationMaps,
} = await this.workspaceCacheService.getOrRecompute(workspaceId, [
'flatObjectMetadataMaps',
'flatFieldMetadataMaps',
'flatApplicationMaps',
]);
const taskTargetFieldUniversalIdentifiers = new Set<string>([
STANDARD_OBJECTS.taskTarget.fields.targetPerson.universalIdentifier,
STANDARD_OBJECTS.taskTarget.fields.targetCompany.universalIdentifier,
STANDARD_OBJECTS.taskTarget.fields.targetOpportunity
.universalIdentifier,
]);
const taskTargetObjectMetadata =
findFlatEntityByUniversalIdentifier<FlatObjectMetadata>({
flatEntityMaps: flatObjectMetadataMaps,
universalIdentifier: STANDARD_OBJECTS.taskTarget.universalIdentifier,
});
if (!taskTargetObjectMetadata) {
this.logger.error(
`🟥 ${tableName} object metadata not found for workspace ${workspaceId}`,
);
return;
}
const taskTargetFieldMetadatas = getFlatFieldsFromFlatObjectMetadata(
taskTargetObjectMetadata,
flatFieldMetadataMaps,
);
const taskTargetRelationFields = taskTargetFieldMetadatas
.filter(isMorphOrRelationFlatFieldMetadata)
.filter((field) => field.type === FieldMetadataType.RELATION)
.filter((field) => {
const isStandardAppField = this.isTwentyStandardApplicationField({
field,
flatApplicationMaps,
workspaceId,
});
const isStandardTarget =
isStandardAppField &&
taskTargetFieldUniversalIdentifiers.has(field.universalIdentifier);
const targetObjectMetadata = field.relationTargetObjectMetadataId
? findFlatEntityByIdInFlatEntityMaps({
flatEntityMaps: flatObjectMetadataMaps,
flatEntityId: field.relationTargetObjectMetadataId,
})
: undefined;
const isCustomTarget =
!isStandardAppField && targetObjectMetadata?.isCustom === true;
return isStandardTarget || isCustomTarget;
});
const fieldMigrations = taskTargetRelationFields.map((field) => {
const newFieldName = `target${capitalize(field.name)}`;
const newFieldLabel = 'Target';
const relationSettings: RelationFieldMetadataSettings = field.settings;
const oldJoinColumnName =
relationSettings?.joinColumnName ??
computeMorphOrRelationFieldJoinColumnName({ name: field.name });
const newJoinColumnName = computeMorphOrRelationFieldJoinColumnName({
name: newFieldName,
});
return {
field,
newFieldName,
newFieldLabel,
oldJoinColumnName,
newJoinColumnName,
};
});
// Rename columns
for (const { oldJoinColumnName, newJoinColumnName } of fieldMigrations) {
if (oldJoinColumnName === newJoinColumnName) {
@@ -250,11 +243,6 @@ export class MigrateTaskTargetToMorphRelationsCommand extends ActiveOrSuspendedW
await queryRunner.commitTransaction();
await this.featureFlagService.enableFeatureFlags(
[FeatureFlagKey.IS_TASK_TARGET_MIGRATED],
workspaceId,
);
const relatedMetadataNames =
getMetadataRelatedMetadataNames('fieldMetadata');
const relatedCacheKeysToInvalidate: WorkspaceCacheKeyName[] =
@@ -262,7 +250,6 @@ export class MigrateTaskTargetToMorphRelationsCommand extends ActiveOrSuspendedW
const cacheKeysToInvalidate: WorkspaceCacheKeyName[] = [
'flatFieldMetadataMaps',
...relatedCacheKeysToInvalidate,
'featureFlagsMap',
];
this.logger.log(
@@ -279,10 +266,6 @@ export class MigrateTaskTargetToMorphRelationsCommand extends ActiveOrSuspendedW
);
this.logger.log(`Cache flushed`);
this.logger.log(
`Set IS_TASK_TARGET_MIGRATED feature flag for workspace ${workspaceId}`,
);
this.logger.log(`Flush cache for workspace ${workspaceId}`);
await this.workspaceCacheStorageService.flush(workspaceId);
} catch (error) {
@@ -4,8 +4,6 @@ import {
} from 'twenty-shared/utils';
import { v4 } from 'uuid';
import { type FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface';
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
import { type CreateObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/create-object.input';
@@ -20,14 +18,12 @@ type FromCreateObjectInputToFlatObjectMetadataAndFlatFieldMetadatasToCreateArgs
{
createObjectInput: CreateObjectInput;
flatApplication: FlatApplication;
existingFeatureFlagsMap: FeatureFlagMap;
} & Pick<AllFlatEntityMaps, 'flatObjectMetadataMaps'>;
export const fromCreateObjectInputToFlatObjectMetadataAndFlatFieldMetadatasToCreate =
({
createObjectInput: rawCreateObjectInput,
flatApplication,
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
existingFeatureFlagsMap,
}: FromCreateObjectInputToFlatObjectMetadataAndFlatFieldMetadatasToCreateArgs): {
flatObjectMetadataToCreate: UniversalFlatObjectMetadata & { id: string };
relationTargetFlatFieldMetadataToCreate: UniversalFlatFieldMetadata[];
@@ -108,7 +104,6 @@ export const fromCreateObjectInputToFlatObjectMetadataAndFlatFieldMetadatasToCre
existingFlatObjectMetadataMaps,
sourceFlatObjectMetadata: universalFlatObjectMetadataToCreate,
flatApplication,
existingFeatureFlagsMap,
});
const objectFlatFieldMetadatas: UniversalFlatFieldMetadata[] = [
@@ -406,7 +406,6 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
createObjectInput,
flatApplication: resolvedOwnerFlatApplication,
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
existingFeatureFlagsMap,
});
const flatDefaultViewToCreate = this.computeFlatViewToCreate({
@@ -2,10 +2,8 @@ import {
STANDARD_OBJECTS,
DEFAULT_RELATIONS_OBJECTS_STANDARD_IDS,
} from 'twenty-shared/metadata';
import { FieldMetadataType, FeatureFlagKey } from 'twenty-shared/types';
import { FieldMetadataType } from 'twenty-shared/types';
import { capitalize, isDefined } from 'twenty-shared/utils';
import { type FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
@@ -34,14 +32,18 @@ const morphIdByRelationObjectNameSingular = {
string | null
>;
// TODO: once we have finished migrating, we can delete custom code
// once we migrate timeline activity to morph relations, we can add it.
// another way to check if an object is migrated to morph relations is to check if the feature flag is enabled
const DEFAULT_MORPH_RELATIONS_OBJECTS_STANDARD_IDS =
[] as const satisfies (keyof typeof STANDARD_OBJECTS)[];
const STANDARD_OBJECT_NAME_SINGULARS_USING_MORPH_ON_CUSTOM_TARGETS =
new Set<string>([
'timelineActivity',
'attachment',
'noteTarget',
'taskTarget',
]);
export type BuildDefaultRelationFieldsForCustomObjectArgs = {
existingFeatureFlagsMap: FeatureFlagMap;
existingFlatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>;
sourceFlatObjectMetadata: UniversalFlatObjectMetadata;
flatApplication: FlatApplication;
@@ -58,7 +60,6 @@ const EMPTY_SOURCE_AND_TARGET_FLAT_FIELD_METADATAS_RECORD: SourceAndTargetFlatFi
};
export const buildDefaultRelationFlatFieldMetadatasForCustomObject = ({
existingFeatureFlagsMap,
existingFlatObjectMetadataMaps,
sourceFlatObjectMetadata,
flatApplication,
@@ -83,17 +84,12 @@ export const buildDefaultRelationFlatFieldMetadatasForCustomObject = ({
DEFAULT_MORPH_RELATIONS_OBJECTS_STANDARD_IDS.map(toString).includes(
objectMetadataNameSingular,
);
const isFeatureFlagEnabled =
objectMetadataNameSingular === 'timelineActivity' ||
(objectMetadataNameSingular === 'attachment' &&
existingFeatureFlagsMap[FeatureFlagKey.IS_ATTACHMENT_MIGRATED]) ||
(objectMetadataNameSingular === 'noteTarget' &&
existingFeatureFlagsMap[FeatureFlagKey.IS_NOTE_TARGET_MIGRATED]) ||
(objectMetadataNameSingular === 'taskTarget' &&
existingFeatureFlagsMap[FeatureFlagKey.IS_TASK_TARGET_MIGRATED]) ||
false;
const usesMorphRelationsForStandardTarget =
STANDARD_OBJECT_NAME_SINGULARS_USING_MORPH_ON_CUSTOM_TARGETS.has(
objectMetadataNameSingular,
);
const isObjectMigratedToMorphRelations =
isObjectMigratedFromOlderReleases || isFeatureFlagEnabled;
isObjectMigratedFromOlderReleases || usesMorphRelationsForStandardTarget;
const targetFlatObjectMetadataId =
objectIdByNameSingular[objectMetadataNameSingular];
@@ -235,9 +235,6 @@ describe('WorkspaceEntityManager', () => {
IS_PUBLIC_DOMAIN_ENABLED: false,
IS_EMAILING_DOMAIN_ENABLED: false,
IS_DASHBOARD_V2_ENABLED: false,
IS_ATTACHMENT_MIGRATED: false,
IS_NOTE_TARGET_MIGRATED: false,
IS_TASK_TARGET_MIGRATED: false,
IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED: false,
IS_JUNCTION_RELATIONS_ENABLED: false,
IS_COMMAND_MENU_ITEM_ENABLED: false,
@@ -50,21 +50,6 @@ export const seedFeatureFlags = async ({
workspaceId: workspaceId,
value: true,
},
{
key: FeatureFlagKey.IS_ATTACHMENT_MIGRATED,
workspaceId: workspaceId,
value: true,
},
{
key: FeatureFlagKey.IS_NOTE_TARGET_MIGRATED,
workspaceId: workspaceId,
value: true,
},
{
key: FeatureFlagKey.IS_TASK_TARGET_MIGRATED,
workspaceId: workspaceId,
value: true,
},
{
key: FeatureFlagKey.IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED,
workspaceId: workspaceId,
@@ -1,8 +1,5 @@
import { FeatureFlagKey } from 'twenty-shared/types';
export const DEFAULT_FEATURE_FLAGS = [
FeatureFlagKey.IS_ATTACHMENT_MIGRATED,
FeatureFlagKey.IS_NOTE_TARGET_MIGRATED,
FeatureFlagKey.IS_TASK_TARGET_MIGRATED,
FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED,
] as const satisfies FeatureFlagKey[];
@@ -8,9 +8,6 @@ export enum FeatureFlagKey {
IS_PUBLIC_DOMAIN_ENABLED = 'IS_PUBLIC_DOMAIN_ENABLED',
IS_EMAILING_DOMAIN_ENABLED = 'IS_EMAILING_DOMAIN_ENABLED',
IS_DASHBOARD_V2_ENABLED = 'IS_DASHBOARD_V2_ENABLED',
IS_ATTACHMENT_MIGRATED = 'IS_ATTACHMENT_MIGRATED',
IS_NOTE_TARGET_MIGRATED = 'IS_NOTE_TARGET_MIGRATED',
IS_TASK_TARGET_MIGRATED = 'IS_TASK_TARGET_MIGRATED',
IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED = 'IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED',
IS_JUNCTION_RELATIONS_ENABLED = 'IS_JUNCTION_RELATIONS_ENABLED',
IS_COMMAND_MENU_ITEM_ENABLED = 'IS_COMMAND_MENU_ITEM_ENABLED',