From e8cb086b64c78670c18459433bcf3384a2c7a3ef Mon Sep 17 00:00:00 2001 From: Charles Bochet Date: Sun, 29 Mar 2026 09:15:26 +0200 Subject: [PATCH] chore: remove completed migration feature flags and upgrade commands <= 1.18 (#19074) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Remove 3 completed migration feature flags: `IS_ATTACHMENT_MIGRATED`, `IS_NOTE_TARGET_MIGRATED`, `IS_TASK_TARGET_MIGRATED` — these were already enabled by default for all new workspaces via `DEFAULT_FEATURE_FLAGS` - Delete all upgrade command directories for versions <= 1.18 (`1-16/`, `1-17/`, `1-18/`) along with their module registrations, removing ~6,600 lines of dead migration code - Simplify frontend utility functions (`getActivityTargetObjectFieldIdName`, `getActivityTargetsFilter`, `getActivityTargetFieldNameForObject`, `generateActivityTargetMorphFieldKeys`, `findActivitiesOperationSignatureFactory`) by removing the `isMorphRelation` parameter and always using the morph relation path - Remove feature flag checks from 7 frontend hooks/components that were gating attachment and activity target behavior behind the removed flags - Simplify `buildDefaultRelationFlatFieldMetadatasForCustomObject` server util to always treat attachment, noteTarget, and taskTarget as morph relations without checking feature flags --- .../src/metadata/generated/schema.graphql | 3 - .../src/metadata/generated/schema.ts | 5 +- .../src/generated-metadata/graphql.ts | 3 - .../activities/files/hooks/useAttachments.tsx | 8 - .../files/hooks/useUploadAttachmentFile.tsx | 7 - ...findActivitiesOperationSignatureFactory.ts | 12 +- .../useActivityTargetsForTargetableObjects.ts | 21 +- .../usePrepareFindManyActivitiesQuery.ts | 14 +- .../hooks/useUpdateActivityTargetFromCell.ts | 15 - .../generateActivityTargetMorphFieldKeys.ts | 3 - .../getActivityTargetFieldNameForObject.ts | 20 +- .../getActivityTargetObjectFieldIdName.ts | 8 +- .../utils/getActivityTargetsFilter.ts | 51 +- .../input/components/RichTextFieldEditor.tsx | 7 - .../components/StandaloneRichTextWidget.tsx | 6 - .../standard-index-field-names.constant.ts | 299 ------- ...kfill-application-package-files.command.ts | 212 ----- ...lete-all-files-and-update-table.command.ts | 142 ---- ...-fix-morph-relation-field-names.command.ts | 337 -------- .../1-17-identify-webhook-metadata.command.ts | 117 --- ...ation-id-not-nullable-migration.command.ts | 71 -- ...e-attachment-to-morph-relations.command.ts | 334 -------- ...rate-date-time-is-filter-values.command.ts | 157 ---- ...-note-target-to-morph-relations.command.ts | 331 -------- ...7-migrate-send-email-recipients.command.ts | 85 -- ...-task-target-to-morph-relations.command.ts | 331 -------- .../1-17-upgrade-version-command.module.ts | 93 --- .../migrate-send-email-step.util.spec.ts | 98 --- .../utils/migrate-send-email-step.util.ts | 95 --- ...ackfill-file-size-and-mime-type.command.ts | 184 ----- ...ge-channel-throttle-retry-after.command.ts | 120 --- ...andard-views-and-field-metadata.command.ts | 753 ----------------- .../1-18-delete-orphan-favorites.command.ts | 87 -- ...y-rich-text-attachment-file-ids.command.ts | 468 ----------- .../1-18-migrate-attachment-files.command.ts | 297 ------- ...orites-to-navigation-menu-items.command.ts | 782 ------------------ ...-18-migrate-person-avatar-files.command.ts | 299 ------- ...workflow-send-email-attachments.command.ts | 211 ----- ...1-18-migrate-workspace-pictures.command.ts | 381 --------- .../1-18-upgrade-version-command.module.ts | 83 -- .../upgrade-version-command.module.ts | 4 - .../upgrade.command.ts | 71 -- ...ade-command-supported-versions.constant.ts | 8 +- ...and-flat-field-metadatas-to-create.util.ts | 5 - .../object-metadata.service.ts | 1 - ...-field-metadatas-for-custom-object.util.ts | 29 +- .../workspace-entity-manager.spec.ts | 3 - .../core/utils/seed-feature-flags.util.ts | 15 - .../constant/default-feature-flags.ts | 8 +- .../twenty-shared/src/types/FeatureFlagKey.ts | 3 - 50 files changed, 29 insertions(+), 6668 deletions(-) delete mode 100644 packages/twenty-server/src/database/commands/upgrade-version-command/1-16/constants/standard-index-field-names.constant.ts delete mode 100644 packages/twenty-server/src/database/commands/upgrade-version-command/1-17/1-17-backfill-application-package-files.command.ts delete mode 100644 packages/twenty-server/src/database/commands/upgrade-version-command/1-17/1-17-delete-all-files-and-update-table.command.ts delete mode 100644 packages/twenty-server/src/database/commands/upgrade-version-command/1-17/1-17-fix-morph-relation-field-names.command.ts delete mode 100644 packages/twenty-server/src/database/commands/upgrade-version-command/1-17/1-17-identify-webhook-metadata.command.ts delete mode 100644 packages/twenty-server/src/database/commands/upgrade-version-command/1-17/1-17-make-webhook-universal-identifier-and-application-id-not-nullable-migration.command.ts delete mode 100644 packages/twenty-server/src/database/commands/upgrade-version-command/1-17/1-17-migrate-attachment-to-morph-relations.command.ts delete mode 100644 packages/twenty-server/src/database/commands/upgrade-version-command/1-17/1-17-migrate-date-time-is-filter-values.command.ts delete mode 100644 packages/twenty-server/src/database/commands/upgrade-version-command/1-17/1-17-migrate-note-target-to-morph-relations.command.ts delete mode 100644 packages/twenty-server/src/database/commands/upgrade-version-command/1-17/1-17-migrate-send-email-recipients.command.ts delete mode 100644 packages/twenty-server/src/database/commands/upgrade-version-command/1-17/1-17-migrate-task-target-to-morph-relations.command.ts delete mode 100644 packages/twenty-server/src/database/commands/upgrade-version-command/1-17/1-17-upgrade-version-command.module.ts delete mode 100644 packages/twenty-server/src/database/commands/upgrade-version-command/1-17/utils/__tests__/migrate-send-email-step.util.spec.ts delete mode 100644 packages/twenty-server/src/database/commands/upgrade-version-command/1-17/utils/migrate-send-email-step.util.ts delete mode 100644 packages/twenty-server/src/database/commands/upgrade-version-command/1-18/1-18-backfill-file-size-and-mime-type.command.ts delete mode 100644 packages/twenty-server/src/database/commands/upgrade-version-command/1-18/1-18-backfill-message-channel-throttle-retry-after.command.ts delete mode 100644 packages/twenty-server/src/database/commands/upgrade-version-command/1-18/1-18-backfill-standard-views-and-field-metadata.command.ts delete mode 100644 packages/twenty-server/src/database/commands/upgrade-version-command/1-18/1-18-delete-orphan-favorites.command.ts delete mode 100644 packages/twenty-server/src/database/commands/upgrade-version-command/1-18/1-18-migrate-activity-rich-text-attachment-file-ids.command.ts delete mode 100644 packages/twenty-server/src/database/commands/upgrade-version-command/1-18/1-18-migrate-attachment-files.command.ts delete mode 100644 packages/twenty-server/src/database/commands/upgrade-version-command/1-18/1-18-migrate-favorites-to-navigation-menu-items.command.ts delete mode 100644 packages/twenty-server/src/database/commands/upgrade-version-command/1-18/1-18-migrate-person-avatar-files.command.ts delete mode 100644 packages/twenty-server/src/database/commands/upgrade-version-command/1-18/1-18-migrate-workflow-send-email-attachments.command.ts delete mode 100644 packages/twenty-server/src/database/commands/upgrade-version-command/1-18/1-18-migrate-workspace-pictures.command.ts delete mode 100644 packages/twenty-server/src/database/commands/upgrade-version-command/1-18/1-18-upgrade-version-command.module.ts diff --git a/packages/twenty-client-sdk/src/metadata/generated/schema.graphql b/packages/twenty-client-sdk/src/metadata/generated/schema.graphql index 4dd0b9a1a7..7a0052b089 100644 --- a/packages/twenty-client-sdk/src/metadata/generated/schema.graphql +++ b/packages/twenty-client-sdk/src/metadata/generated/schema.graphql @@ -1731,9 +1731,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 diff --git a/packages/twenty-client-sdk/src/metadata/generated/schema.ts b/packages/twenty-client-sdk/src/metadata/generated/schema.ts index 57b0e4f1a5..924402b794 100644 --- a/packages/twenty-client-sdk/src/metadata/generated/schema.ts +++ b/packages/twenty-client-sdk/src/metadata/generated/schema.ts @@ -1427,7 +1427,7 @@ export interface PublicFeatureFlag { __typename: 'PublicFeatureFlag' } -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' | 'IS_GRAPHQL_QUERY_TIMING_ENABLED' | 'IS_RECORD_TABLE_WIDGET_ENABLED' | 'IS_DATASOURCE_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' | 'IS_GRAPHQL_QUERY_TIMING_ENABLED' | 'IS_RECORD_TABLE_WIDGET_ENABLED' | 'IS_DATASOURCE_MIGRATED' export interface ClientConfig { appVersion?: Scalars['String'] @@ -9147,9 +9147,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, diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index 280545c26e..fcd610c968 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -1703,7 +1703,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', @@ -1718,14 +1717,12 @@ 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_RECORD_TABLE_WIDGET_ENABLED = 'IS_RECORD_TABLE_WIDGET_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' } diff --git a/packages/twenty-front/src/modules/activities/files/hooks/useAttachments.tsx b/packages/twenty-front/src/modules/activities/files/hooks/useAttachments.tsx index 702feca6b6..caa3889dad 100644 --- a/packages/twenty-front/src/modules/activities/files/hooks/useAttachments.tsx +++ b/packages/twenty-front/src/modules/activities/files/hooks/useAttachments.tsx @@ -3,17 +3,9 @@ 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, }); const { records: attachments, loading } = useFindManyRecords({ diff --git a/packages/twenty-front/src/modules/activities/files/hooks/useUploadAttachmentFile.tsx b/packages/twenty-front/src/modules/activities/files/hooks/useUploadAttachmentFile.tsx index b683873c80..8240510c55 100644 --- a/packages/twenty-front/src/modules/activities/files/hooks/useUploadAttachmentFile.tsx +++ b/packages/twenty-front/src/modules/activities/files/hooks/useUploadAttachmentFile.tsx @@ -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,6 @@ export const useUploadAttachmentFile = () => { const targetableObjectFieldIdName = getActivityTargetObjectFieldIdName({ nameSingular: targetableObject.targetObjectNameSingular, - isMorphRelation: isAttachmentMigrated, }); const attachmentToCreate = { diff --git a/packages/twenty-front/src/modules/activities/graphql/operation-signatures/factories/findActivitiesOperationSignatureFactory.ts b/packages/twenty-front/src/modules/activities/graphql/operation-signatures/factories/findActivitiesOperationSignatureFactory.ts index bda41410d7..59ea1fdd20 100644 --- a/packages/twenty-front/src/modules/activities/graphql/operation-signatures/factories/findActivitiesOperationSignatureFactory.ts +++ b/packages/twenty-front/src/modules/activities/graphql/operation-signatures/factories/findActivitiesOperationSignatureFactory.ts @@ -6,7 +6,6 @@ import { type RecordGqlOperationSignatureFactory } from '@/object-record/graphql type FindActivitiesOperationSignatureFactory = { objectMetadataItems: EnrichedObjectMetadataItem[]; objectNameSingular: CoreObjectNameSingular; - isMorphRelation: boolean; }; export const findActivitiesOperationSignatureFactory: RecordGqlOperationSignatureFactory< @@ -14,7 +13,6 @@ export const findActivitiesOperationSignatureFactory: RecordGqlOperationSignatur > = ({ objectMetadataItems, objectNameSingular, - isMorphRelation, }: FindActivitiesOperationSignatureFactory) => { const body = { bodyV2: { @@ -67,10 +65,7 @@ export const findActivitiesOperationSignatureFactory: RecordGqlOperationSignatur deletedAt: true, note: true, noteId: true, - ...generateActivityTargetMorphFieldKeys( - objectMetadataItems, - isMorphRelation, - ), + ...generateActivityTargetMorphFieldKeys(objectMetadataItems), }, } : { @@ -82,10 +77,7 @@ export const findActivitiesOperationSignatureFactory: RecordGqlOperationSignatur deletedAt: true, task: true, taskId: true, - ...generateActivityTargetMorphFieldKeys( - objectMetadataItems, - isMorphRelation, - ), + ...generateActivityTargetMorphFieldKeys(objectMetadataItems), }, }), }, diff --git a/packages/twenty-front/src/modules/activities/hooks/useActivityTargetsForTargetableObjects.ts b/packages/twenty-front/src/modules/activities/hooks/useActivityTargetsForTargetableObjects.ts index 9930a254f2..9964dfe483 100644 --- a/packages/twenty-front/src/modules/activities/hooks/useActivityTargetsForTargetableObjects.ts +++ b/packages/twenty-front/src/modules/activities/hooks/useActivityTargetsForTargetableObjects.ts @@ -1,5 +1,5 @@ import { - CoreObjectNameSingular, + type CoreObjectNameSingular, type RecordGqlOperationOrderBy, } from 'twenty-shared/types'; @@ -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,8 @@ export const useActivityTargetsForTargetableObjects = ({ const objectMetadataItems = useAtomStateValue( 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, + targetableObjects, }); const FIND_ACTIVITY_TARGETS_OPERATION_SIGNATURE = diff --git a/packages/twenty-front/src/modules/activities/hooks/usePrepareFindManyActivitiesQuery.ts b/packages/twenty-front/src/modules/activities/hooks/usePrepareFindManyActivitiesQuery.ts index b75c94763f..f54a5a18db 100644 --- a/packages/twenty-front/src/modules/activities/hooks/usePrepareFindManyActivitiesQuery.ts +++ b/packages/twenty-front/src/modules/activities/hooks/usePrepareFindManyActivitiesQuery.ts @@ -7,15 +7,13 @@ import { type TaskTarget } from '@/activities/types/TaskTarget'; import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; -import { CoreObjectNameSingular } from 'twenty-shared/types'; +import { type CoreObjectNameSingular } from 'twenty-shared/types'; import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache'; import { useUpsertFindManyRecordsQueryInCache } from '@/object-record/cache/hooks/useUpsertFindManyRecordsQueryInCache'; 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,6 @@ export const usePrepareFindManyActivitiesQuery = ({ findActivitiesOperationSignatureFactory({ objectNameSingular: activityObjectNameSingular, objectMetadataItems, - isMorphRelation: - activityObjectNameSingular === CoreObjectNameSingular.Task - ? isTaskTargetMigrated - : isNoteTargetMigrated, }); upsertFindManyActivitiesInCache({ diff --git a/packages/twenty-front/src/modules/activities/inline-cell/hooks/useUpdateActivityTargetFromCell.ts b/packages/twenty-front/src/modules/activities/inline-cell/hooks/useUpdateActivityTargetFromCell.ts index 6ab741b6a0..08c9abc554 100644 --- a/packages/twenty-front/src/modules/activities/inline-cell/hooks/useUpdateActivityTargetFromCell.ts +++ b/packages/twenty-front/src/modules/activities/inline-cell/hooks/useUpdateActivityTargetFromCell.ts @@ -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,6 @@ export const useUpdateActivityTargetFromCell = ({ activityObjectNameSingular, targetObjectMetadataId: morphItem.objectMetadataId, objectMetadataItems, - isMorphRelation, }); if (!isDefined(targetFieldName)) { @@ -211,7 +197,6 @@ export const useUpdateActivityTargetFromCell = ({ activityObjectNameSingular, createOneActivityTarget, deleteOneActivityTarget, - isMorphRelation, setRecordStore, ], ); diff --git a/packages/twenty-front/src/modules/activities/utils/generateActivityTargetMorphFieldKeys.ts b/packages/twenty-front/src/modules/activities/utils/generateActivityTargetMorphFieldKeys.ts index bfd908b6e0..03f12a7dc7 100644 --- a/packages/twenty-front/src/modules/activities/utils/generateActivityTargetMorphFieldKeys.ts +++ b/packages/twenty-front/src/modules/activities/utils/generateActivityTargetMorphFieldKeys.ts @@ -3,7 +3,6 @@ import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/Enriche export const generateActivityTargetMorphFieldKeys = ( objectMetadataItems: EnrichedObjectMetadataItem[], - isMorphRelation: boolean, ) => { const targetableObjectMetadataItems = objectMetadataItems.filter( (objectMetadataItem) => @@ -14,7 +13,6 @@ export const generateActivityTargetMorphFieldKeys = ( targetableObjectMetadataItems.map((objectMetadataItem) => { const targetFieldIdName = getActivityTargetObjectFieldIdName({ nameSingular: objectMetadataItem.nameSingular, - isMorphRelation, }); return [targetFieldIdName.replace(/Id$/, ''), true]; @@ -25,7 +23,6 @@ export const generateActivityTargetMorphFieldKeys = ( targetableObjectMetadataItems.map((objectMetadataItem) => { const targetFieldIdName = getActivityTargetObjectFieldIdName({ nameSingular: objectMetadataItem.nameSingular, - isMorphRelation, }); return [targetFieldIdName, true]; diff --git a/packages/twenty-front/src/modules/activities/utils/getActivityTargetFieldNameForObject.ts b/packages/twenty-front/src/modules/activities/utils/getActivityTargetFieldNameForObject.ts index b476dc1ba7..31329e2eeb 100644 --- a/packages/twenty-front/src/modules/activities/utils/getActivityTargetFieldNameForObject.ts +++ b/packages/twenty-front/src/modules/activities/utils/getActivityTargetFieldNameForObject.ts @@ -9,14 +9,12 @@ type GetActivityTargetFieldNameForObjectArgs = { | CoreObjectNameSingular.Task; targetObjectMetadataId: string; objectMetadataItems: EnrichedObjectMetadataItem[]; - isMorphRelation?: boolean; }; export const getActivityTargetFieldNameForObject = ({ activityObjectNameSingular, targetObjectMetadataId, objectMetadataItems, - isMorphRelation = false, }: GetActivityTargetFieldNameForObjectArgs): string | undefined => { const activityTargetObjectNameSingular = activityObjectNameSingular === CoreObjectNameSingular.Task @@ -36,17 +34,13 @@ export const getActivityTargetFieldNameForObject = ({ (objectMetadataItem) => objectMetadataItem.id === targetObjectMetadataId, ); - if (isMorphRelation && isDefined(targetObjectMetadataItem)) { - const fieldIdName = getActivityTargetObjectFieldIdName({ - nameSingular: targetObjectMetadataItem.nameSingular, - isMorphRelation: true, - }); - - return fieldIdName.replace(/Id$/, ''); + if (!isDefined(targetObjectMetadataItem)) { + return undefined; } - return activityTargetObjectMetadata.fields.find( - (field) => - field.relation?.targetObjectMetadata.id === targetObjectMetadataId, - )?.name; + const fieldIdName = getActivityTargetObjectFieldIdName({ + nameSingular: targetObjectMetadataItem.nameSingular, + }); + + return fieldIdName.replace(/Id$/, ''); }; diff --git a/packages/twenty-front/src/modules/activities/utils/getActivityTargetObjectFieldIdName.ts b/packages/twenty-front/src/modules/activities/utils/getActivityTargetObjectFieldIdName.ts index 71384a2099..7a69b12ace 100644 --- a/packages/twenty-front/src/modules/activities/utils/getActivityTargetObjectFieldIdName.ts +++ b/packages/twenty-front/src/modules/activities/utils/getActivityTargetObjectFieldIdName.ts @@ -2,14 +2,8 @@ import { capitalize } from 'twenty-shared/utils'; export const getActivityTargetObjectFieldIdName = ({ nameSingular, - isMorphRelation = false, }: { nameSingular: string; - isMorphRelation?: boolean; }) => { - if (isMorphRelation) { - return `target${capitalize(nameSingular)}Id`; - } - - return `${nameSingular}Id`; + return `target${capitalize(nameSingular)}Id`; }; diff --git a/packages/twenty-front/src/modules/activities/utils/getActivityTargetsFilter.ts b/packages/twenty-front/src/modules/activities/utils/getActivityTargetsFilter.ts index 1a9a46ef5d..62e4e8dd3f 100644 --- a/packages/twenty-front/src/modules/activities/utils/getActivityTargetsFilter.ts +++ b/packages/twenty-front/src/modules/activities/utils/getActivityTargetsFilter.ts @@ -1,58 +1,21 @@ import { type ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getActivityTargetObjectFieldIdName'; -import { CoreObjectNameSingular } from 'twenty-shared/types'; -import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem'; import { isDefined } from 'twenty-shared/utils'; export const getActivityTargetsFilter = ({ targetableObjects, - activityObjectNameSingular, - objectMetadataItems, - isMorphRelation = false, }: { - targetableObjects: ActivityTargetableObject[]; - activityObjectNameSingular: - | CoreObjectNameSingular.Note - | CoreObjectNameSingular.Task; - objectMetadataItems: EnrichedObjectMetadataItem[]; - isMorphRelation?: boolean; + targetableObjects: Pick< + ActivityTargetableObject, + 'id' | 'targetObjectNameSingular' + >[]; }) => { - const activityTargetObjectNameSingular = - activityObjectNameSingular === CoreObjectNameSingular.Task - ? CoreObjectNameSingular.TaskTarget - : CoreObjectNameSingular.NoteTarget; - - const activityTargetObjectMetadata = objectMetadataItems.find( - (objectMetadataItem) => - objectMetadataItem.nameSingular === activityTargetObjectNameSingular, - ); - const findManyActivityTargetsQueryFilter = Object.fromEntries( targetableObjects .map((targetableObject) => { - const targetObjectMetadataItem = objectMetadataItems.find( - (item) => - item.nameSingular === targetableObject.targetObjectNameSingular, - ); - - if (!isDefined(targetObjectMetadataItem)) { - return undefined; - } - - const joinColumnName = isMorphRelation - ? getActivityTargetObjectFieldIdName({ - nameSingular: targetObjectMetadataItem.nameSingular, - isMorphRelation: true, - }) - : activityTargetObjectMetadata?.fields.find( - (field) => - field.relation?.targetObjectMetadata.id === - targetObjectMetadataItem.id, - )?.settings?.joinColumnName; - - if (!isDefined(joinColumnName)) { - return undefined; - } + const joinColumnName = getActivityTargetObjectFieldIdName({ + nameSingular: targetableObject.targetObjectNameSingular, + }); return [ joinColumnName, diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/RichTextFieldEditor.tsx b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/RichTextFieldEditor.tsx index 276880c25b..0b44d10cee 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/RichTextFieldEditor.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/RichTextFieldEditor.tsx @@ -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,7 +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; @@ -82,13 +80,8 @@ export const RichTextFieldEditor = ({ const focusId = `${recordId}-${fieldName}`; - const isAttachmentMigrated = useIsFeatureEnabled( - FeatureFlagKey.IS_ATTACHMENT_MIGRATED, - ); - const attachmentTargetFieldIdName = getActivityTargetObjectFieldIdName({ nameSingular: objectNameSingular, - isMorphRelation: isAttachmentMigrated, }); const { records: attachments } = useFindManyRecords({ diff --git a/packages/twenty-front/src/modules/page-layout/widgets/standalone-rich-text/components/StandaloneRichTextWidget.tsx b/packages/twenty-front/src/modules/page-layout/widgets/standalone-rich-text/components/StandaloneRichTextWidget.tsx index ea837585bd..64dc723769 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/standalone-rich-text/components/StandaloneRichTextWidget.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/standalone-rich-text/components/StandaloneRichTextWidget.tsx @@ -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,11 @@ 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, }); const configuration = widget.configuration as diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-16/constants/standard-index-field-names.constant.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-16/constants/standard-index-field-names.constant.ts deleted file mode 100644 index d793b15a68..0000000000 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-16/constants/standard-index-field-names.constant.ts +++ /dev/null @@ -1,299 +0,0 @@ -import { STANDARD_OBJECTS } from 'twenty-shared/metadata'; - -// Maps indexName to its related field universal identifiers for each standard object -export const STANDARD_INDEX_FIELD_UNIVERSAL_IDENTIFIERS: Record< - string, - Record -> = { - attachment: { - taskIdIndex: [ - STANDARD_OBJECTS.attachment.fields.targetTask.universalIdentifier, - ], - noteIdIndex: [ - STANDARD_OBJECTS.attachment.fields.targetNote.universalIdentifier, - ], - personIdIndex: [ - STANDARD_OBJECTS.attachment.fields.targetPerson.universalIdentifier, - ], - companyIdIndex: [ - STANDARD_OBJECTS.attachment.fields.targetCompany.universalIdentifier, - ], - opportunityIdIndex: [ - STANDARD_OBJECTS.attachment.fields.targetOpportunity.universalIdentifier, - ], - dashboardIdIndex: [ - STANDARD_OBJECTS.attachment.fields.targetDashboard.universalIdentifier, - ], - workflowIdIndex: [ - STANDARD_OBJECTS.attachment.fields.targetWorkflow.universalIdentifier, - ], - }, - blocklist: { - workspaceMemberIdIndex: [ - STANDARD_OBJECTS.blocklist.fields.workspaceMember.universalIdentifier, - ], - }, - calendarChannelEventAssociation: { - calendarChannelIdIndex: [ - STANDARD_OBJECTS.calendarChannelEventAssociation.fields.calendarChannel - .universalIdentifier, - ], - calendarEventIdIndex: [ - STANDARD_OBJECTS.calendarChannelEventAssociation.fields.calendarEvent - .universalIdentifier, - ], - }, - calendarChannel: { - connectedAccountIdIndex: [ - STANDARD_OBJECTS.calendarChannel.fields.connectedAccount - .universalIdentifier, - ], - }, - calendarEventParticipant: { - calendarEventIdIndex: [ - STANDARD_OBJECTS.calendarEventParticipant.fields.calendarEvent - .universalIdentifier, - ], - personIdIndex: [ - STANDARD_OBJECTS.calendarEventParticipant.fields.person - .universalIdentifier, - ], - workspaceMemberIdIndex: [ - STANDARD_OBJECTS.calendarEventParticipant.fields.workspaceMember - .universalIdentifier, - ], - }, - company: { - accountOwnerIdIndex: [ - STANDARD_OBJECTS.company.fields.accountOwner.universalIdentifier, - ], - domainNameUniqueIndex: [ - STANDARD_OBJECTS.company.fields.domainName.universalIdentifier, - ], - searchVectorGinIndex: [ - STANDARD_OBJECTS.company.fields.searchVector.universalIdentifier, - ], - }, - connectedAccount: { - accountOwnerIdIndex: [ - STANDARD_OBJECTS.connectedAccount.fields.accountOwner.universalIdentifier, - ], - }, - dashboard: { - searchVectorGinIndex: [ - STANDARD_OBJECTS.dashboard.fields.searchVector.universalIdentifier, - ], - }, - favorite: { - forWorkspaceMemberIdIndex: [ - STANDARD_OBJECTS.favorite.fields.forWorkspaceMember.universalIdentifier, - ], - personIdIndex: [ - STANDARD_OBJECTS.favorite.fields.person.universalIdentifier, - ], - companyIdIndex: [ - STANDARD_OBJECTS.favorite.fields.company.universalIdentifier, - ], - favoriteFolderIdIndex: [ - STANDARD_OBJECTS.favorite.fields.favoriteFolder.universalIdentifier, - ], - opportunityIdIndex: [ - STANDARD_OBJECTS.favorite.fields.opportunity.universalIdentifier, - ], - workflowIdIndex: [ - STANDARD_OBJECTS.favorite.fields.workflow.universalIdentifier, - ], - workflowVersionIdIndex: [ - STANDARD_OBJECTS.favorite.fields.workflowVersion.universalIdentifier, - ], - workflowRunIdIndex: [ - STANDARD_OBJECTS.favorite.fields.workflowRun.universalIdentifier, - ], - taskIdIndex: [STANDARD_OBJECTS.favorite.fields.task.universalIdentifier], - noteIdIndex: [STANDARD_OBJECTS.favorite.fields.note.universalIdentifier], - dashboardIdIndex: [ - STANDARD_OBJECTS.favorite.fields.dashboard.universalIdentifier, - ], - }, - messageChannelMessageAssociation: { - messageChannelIdIndex: [ - STANDARD_OBJECTS.messageChannelMessageAssociation.fields.messageChannel - .universalIdentifier, - ], - messageIdIndex: [ - STANDARD_OBJECTS.messageChannelMessageAssociation.fields.message - .universalIdentifier, - ], - messageChannelIdMessageIdUniqueIndex: [ - STANDARD_OBJECTS.messageChannelMessageAssociation.fields.messageChannel - .universalIdentifier, - STANDARD_OBJECTS.messageChannelMessageAssociation.fields.message - .universalIdentifier, - ], - }, - messageChannel: { - connectedAccountIdIndex: [ - STANDARD_OBJECTS.messageChannel.fields.connectedAccount - .universalIdentifier, - ], - }, - messageFolder: { - messageChannelIdIndex: [ - STANDARD_OBJECTS.messageFolder.fields.messageChannel.universalIdentifier, - ], - }, - messageParticipant: { - messageIdIndex: [ - STANDARD_OBJECTS.messageParticipant.fields.message.universalIdentifier, - ], - personIdIndex: [ - STANDARD_OBJECTS.messageParticipant.fields.person.universalIdentifier, - ], - workspaceMemberIdIndex: [ - STANDARD_OBJECTS.messageParticipant.fields.workspaceMember - .universalIdentifier, - ], - }, - message: { - messageThreadIdIndex: [ - STANDARD_OBJECTS.message.fields.messageThread.universalIdentifier, - ], - }, - note: { - searchVectorGinIndex: [ - STANDARD_OBJECTS.note.fields.searchVector.universalIdentifier, - ], - }, - noteTarget: { - noteIdIndex: [STANDARD_OBJECTS.noteTarget.fields.note.universalIdentifier], - personIdIndex: [ - STANDARD_OBJECTS.noteTarget.fields.targetPerson.universalIdentifier, - ], - companyIdIndex: [ - STANDARD_OBJECTS.noteTarget.fields.targetCompany.universalIdentifier, - ], - opportunityIdIndex: [ - STANDARD_OBJECTS.noteTarget.fields.targetOpportunity.universalIdentifier, - ], - }, - opportunity: { - pointOfContactIdIndex: [ - STANDARD_OBJECTS.opportunity.fields.pointOfContact.universalIdentifier, - ], - companyIdIndex: [ - STANDARD_OBJECTS.opportunity.fields.company.universalIdentifier, - ], - stageIndex: [STANDARD_OBJECTS.opportunity.fields.stage.universalIdentifier], - searchVectorGinIndex: [ - STANDARD_OBJECTS.opportunity.fields.searchVector.universalIdentifier, - ], - }, - person: { - companyIdIndex: [ - STANDARD_OBJECTS.person.fields.company.universalIdentifier, - ], - emailsUniqueIndex: [ - STANDARD_OBJECTS.person.fields.emails.universalIdentifier, - ], - searchVectorGinIndex: [ - STANDARD_OBJECTS.person.fields.searchVector.universalIdentifier, - ], - }, - task: { - assigneeIdIndex: [ - STANDARD_OBJECTS.task.fields.assignee.universalIdentifier, - ], - searchVectorGinIndex: [ - STANDARD_OBJECTS.task.fields.searchVector.universalIdentifier, - ], - }, - taskTarget: { - taskIdIndex: [STANDARD_OBJECTS.taskTarget.fields.task.universalIdentifier], - personIdIndex: [ - STANDARD_OBJECTS.taskTarget.fields.targetPerson.universalIdentifier, - ], - companyIdIndex: [ - STANDARD_OBJECTS.taskTarget.fields.targetCompany.universalIdentifier, - ], - opportunityIdIndex: [ - STANDARD_OBJECTS.taskTarget.fields.targetOpportunity.universalIdentifier, - ], - }, - timelineActivity: { - workspaceMemberIdIndex: [ - STANDARD_OBJECTS.timelineActivity.fields.workspaceMember - .universalIdentifier, - ], - personIdIndex: [ - STANDARD_OBJECTS.timelineActivity.fields.targetPerson.universalIdentifier, - ], - companyIdIndex: [ - STANDARD_OBJECTS.timelineActivity.fields.targetCompany - .universalIdentifier, - ], - opportunityIdIndex: [ - STANDARD_OBJECTS.timelineActivity.fields.targetOpportunity - .universalIdentifier, - ], - noteIdIndex: [ - STANDARD_OBJECTS.timelineActivity.fields.targetNote.universalIdentifier, - ], - taskIdIndex: [ - STANDARD_OBJECTS.timelineActivity.fields.targetTask.universalIdentifier, - ], - workflowIdIndex: [ - STANDARD_OBJECTS.timelineActivity.fields.targetWorkflow - .universalIdentifier, - ], - workflowVersionIdIndex: [ - STANDARD_OBJECTS.timelineActivity.fields.targetWorkflowVersion - .universalIdentifier, - ], - workflowRunIdIndex: [ - STANDARD_OBJECTS.timelineActivity.fields.targetWorkflowRun - .universalIdentifier, - ], - dashboardIdIndex: [ - STANDARD_OBJECTS.timelineActivity.fields.targetDashboard - .universalIdentifier, - ], - }, - workflow: { - searchVectorGinIndex: [ - STANDARD_OBJECTS.workflow.fields.searchVector.universalIdentifier, - ], - }, - workflowAutomatedTrigger: { - workflowIdIndex: [ - STANDARD_OBJECTS.workflowAutomatedTrigger.fields.workflow - .universalIdentifier, - ], - }, - workflowRun: { - workflowVersionIdIndex: [ - STANDARD_OBJECTS.workflowRun.fields.workflowVersion.universalIdentifier, - ], - workflowIdIndex: [ - STANDARD_OBJECTS.workflowRun.fields.workflow.universalIdentifier, - ], - searchVectorGinIndex: [ - STANDARD_OBJECTS.workflowRun.fields.searchVector.universalIdentifier, - ], - }, - workflowVersion: { - workflowIdIndex: [ - STANDARD_OBJECTS.workflowVersion.fields.workflow.universalIdentifier, - ], - searchVectorGinIndex: [ - STANDARD_OBJECTS.workflowVersion.fields.searchVector.universalIdentifier, - ], - }, - workspaceMember: { - userEmailUniqueIndex: [ - STANDARD_OBJECTS.workspaceMember.fields.userEmail.universalIdentifier, - ], - searchVectorGinIndex: [ - STANDARD_OBJECTS.workspaceMember.fields.searchVector.universalIdentifier, - ], - }, -}; diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-17/1-17-backfill-application-package-files.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-17/1-17-backfill-application-package-files.command.ts deleted file mode 100644 index 129a38adf5..0000000000 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-17/1-17-backfill-application-package-files.command.ts +++ /dev/null @@ -1,212 +0,0 @@ -import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; - -import { isNonEmptyString } from '@sniptt/guards'; -import { Command } from 'nest-commander'; -import { FileFolder } from 'twenty-shared/types'; -import { isDefined } from 'twenty-shared/utils'; -import { DataSource, Repository } from 'typeorm'; - -import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner'; -import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner'; -import { ApplicationService } from 'src/engine/core-modules/application/application.service'; -import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type'; -import { parseAvailablePackagesFromPackageJsonAndYarnLock } from 'src/engine/core-modules/application/application-package/utils/parse-available-packages-from-package-json-and-yarn-lock.util'; -import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service'; -import { FileSettings } from 'src/engine/core-modules/file/types/file-settings.types'; -import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; -import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; -import { LogicFunctionLayerEntity } from 'src/engine/metadata-modules/logic-function-layer/logic-function-layer.entity'; -import { logicFunctionCreateHash } from 'src/engine/metadata-modules/logic-function/utils/logic-function-create-hash.utils'; -import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; -import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; - -@Command({ - name: 'upgrade:1-17:backfill-application-package-files', - description: - 'Backfill application package files: standard/custom apps get default files, other apps get files from logic function layer', -}) -export class BackfillApplicationPackageFilesCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner { - constructor( - @InjectRepository(WorkspaceEntity) - protected readonly workspaceRepository: Repository, - protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager, - protected readonly dataSourceService: DataSourceService, - protected readonly applicationService: ApplicationService, - protected readonly fileStorageService: FileStorageService, - protected readonly workspaceCacheService: WorkspaceCacheService, - @InjectDataSource() - private readonly coreDataSource: DataSource, - ) { - super(workspaceRepository, twentyORMGlobalManager, dataSourceService); - } - - override async runOnWorkspace({ - workspaceId, - options, - }: RunOnWorkspaceArgs): Promise { - this.logger.log( - `Running backfill application package files for workspace ${workspaceId}`, - ); - - const dryRun = options.dryRun ?? false; - const workspace = await this.workspaceRepository.findOne({ - where: { id: workspaceId }, - select: ['id', 'workspaceCustomApplicationId'], - }); - - if (!isDefined(workspace)) { - this.logger.warn(`Workspace ${workspaceId} not found, skipping`); - - return; - } - - const { twentyStandardFlatApplication, workspaceCustomFlatApplication } = - await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow( - { workspaceId }, - ); - - const { flatApplicationMaps } = - await this.workspaceCacheService.getOrRecompute(workspaceId, [ - 'flatApplicationMaps', - ]); - - const flatApplications = Object.values( - flatApplicationMaps.byId, - ) as FlatApplication[]; - - for (const application of flatApplications) { - const isStandardOrCustomApplication = - application.id === twentyStandardFlatApplication.id || - application.id === workspaceCustomFlatApplication.id; - - if (isStandardOrCustomApplication) { - const needsBackfill = - !isNonEmptyString(application.packageJsonFileId) || - !isNonEmptyString(application.yarnLockFileId) || - !isNonEmptyString(application.packageJsonChecksum) || - !isNonEmptyString(application.yarnLockChecksum) || - !isNonEmptyString(application.availablePackages); - - if (needsBackfill) { - this.logger.log( - `Backfilling standard/custom application ${application.id} with default package files`, - ); - - if (!dryRun) { - await this.applicationService.uploadDefaultPackageFilesAndSetFileIds( - application, - ); - } - } else { - this.logger.log( - `Skipping standard/custom application ${application.id} - already has package files`, - ); - } - - continue; - } - - const hasLogicFunctionLayer = isDefined(application.logicFunctionLayerId); - const needsBackfill = - !isNonEmptyString(application.packageJsonFileId) || - !isNonEmptyString(application.yarnLockFileId) || - !isNonEmptyString(application.packageJsonChecksum) || - !isNonEmptyString(application.yarnLockChecksum) || - !isNonEmptyString(application.availablePackages); - - if (!hasLogicFunctionLayer) { - this.logger.log( - `Skipping application ${application.id} - no logic function layer`, - ); - - continue; - } - - if (!needsBackfill) { - this.logger.log( - `Skipping application ${application.id} - already has package files`, - ); - - continue; - } - - this.logger.log( - `Backfilling application ${application.id} from logic function layer ${application.logicFunctionLayerId}`, - ); - - if (!dryRun) { - await this.backfillApplicationFromLogicFunctionLayer(application); - } - } - } - - private async backfillApplicationFromLogicFunctionLayer( - application: Pick< - FlatApplication, - 'id' | 'universalIdentifier' | 'workspaceId' | 'logicFunctionLayerId' - >, - ): Promise { - const layerRepository = this.coreDataSource.getRepository( - LogicFunctionLayerEntity, - ); - const layer = await layerRepository.findOne({ - where: { - id: application.logicFunctionLayerId as string, - workspaceId: application.workspaceId, - }, - select: ['id', 'packageJson', 'yarnLock'], - }); - - if (!isDefined(layer)) { - this.logger.warn( - `Logic function layer ${application.logicFunctionLayerId} not found for application ${application.id}, skipping`, - ); - - return; - } - - const packageJsonContent = JSON.stringify(layer.packageJson, null, 2); - const packageJsonChecksum = logicFunctionCreateHash( - JSON.stringify(layer.packageJson), - ); - const yarnLockChecksum = logicFunctionCreateHash(layer.yarnLock); - const availablePackages = parseAvailablePackagesFromPackageJsonAndYarnLock( - packageJsonContent, - layer.yarnLock, - ); - - const dependencyFileSettings: FileSettings = { - isTemporaryFile: false, - toDelete: false, - }; - - const packageJsonFile = await this.fileStorageService.writeFile({ - sourceFile: packageJsonContent, - mimeType: undefined, - fileFolder: FileFolder.Dependencies, - applicationUniversalIdentifier: application.universalIdentifier, - workspaceId: application.workspaceId, - resourcePath: 'package.json', - settings: dependencyFileSettings, - }); - - const yarnLockFile = await this.fileStorageService.writeFile({ - sourceFile: layer.yarnLock, - mimeType: undefined, - fileFolder: FileFolder.Dependencies, - applicationUniversalIdentifier: application.universalIdentifier, - workspaceId: application.workspaceId, - resourcePath: 'yarn.lock', - settings: dependencyFileSettings, - }); - - await this.applicationService.update(application.id, { - packageJsonFileId: packageJsonFile.id, - yarnLockFileId: yarnLockFile.id, - packageJsonChecksum, - yarnLockChecksum, - availablePackages, - workspaceId: application.workspaceId, - }); - } -} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-17/1-17-delete-all-files-and-update-table.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-17/1-17-delete-all-files-and-update-table.command.ts deleted file mode 100644 index aa5f2c6c05..0000000000 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-17/1-17-delete-all-files-and-update-table.command.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { Logger } from '@nestjs/common'; -import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; - -import { Command } from 'nest-commander'; -import { DataSource, Repository } from 'typeorm'; - -import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner'; -import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner'; -import { updateFileTableQueries } from 'src/database/typeorm/core/migrations/utils/1768572831179-updateFileTable.util'; -import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity'; -import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; -import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; -import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; - -@Command({ - name: 'upgrade:delete-file-records-and-update-table', - description: 'Delete all file records and update file table schema', -}) -export class DeleteFileRecordsAndUpdateTableCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner { - protected readonly logger = new Logger( - DeleteFileRecordsAndUpdateTableCommand.name, - ); - private hasRunOnce = false; - - constructor( - @InjectRepository(WorkspaceEntity) - protected readonly workspaceRepository: Repository, - @InjectRepository(FileEntity) - private readonly fileRepository: Repository, - protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager, - protected readonly dataSourceService: DataSourceService, - @InjectDataSource() - private readonly coreDataSource: DataSource, - ) { - super(workspaceRepository, twentyORMGlobalManager, dataSourceService); - } - - override async runOnWorkspace(args: RunOnWorkspaceArgs): Promise { - if (this.hasRunOnce) { - return; - } - - await this.deleteFileRecords(args); - await this.updateFileTable(); - await this.addFileEntityUniqueConstraint(args); - this.hasRunOnce = true; - } - - private async updateFileTable(): Promise { - const queryRunner = this.coreDataSource.createQueryRunner(); - - await queryRunner.connect(); - await queryRunner.startTransaction(); - - try { - await updateFileTableQueries(queryRunner); - - await queryRunner.commitTransaction(); - this.logger.log('Successfully updated file table'); - } catch (error) { - await queryRunner.rollbackTransaction(); - this.logger.error(`Rolling back updateFileTable: ${error.message}`); - } finally { - await queryRunner.release(); - } - } - - private async deleteFileRecords({ - workspaceId, - options, - }: RunOnWorkspaceArgs): Promise { - const isDryRun = options.dryRun || false; - - this.logger.log( - `${isDryRun ? '[DRY RUN] ' : ''}Starting deletion of file records for workspace ${workspaceId}`, - ); - - const files = await this.fileRepository.find({ - select: ['id'], - withDeleted: true, - }); - - if (files.length === 0) { - this.logger.log( - `No file records found for workspace ${workspaceId}, skipping`, - ); - - return; - } - - this.logger.log( - `${isDryRun ? '[DRY RUN] Would delete' : 'Deleting'} ${files.length} file record(s) for workspace ${workspaceId}`, - ); - - if (isDryRun) { - return; - } - - try { - await this.fileRepository.delete({ workspaceId }); - - this.logger.log( - `Successfully deleted ${files.length} file record(s) for workspace ${workspaceId}`, - ); - } catch (error) { - this.logger.error( - `Failed to delete file records for workspace ${workspaceId}: ${error.message}`, - ); - - throw error; - } - } - - private async addFileEntityUniqueConstraint({ - options, - }: RunOnWorkspaceArgs): Promise { - if (options.dryRun) { - return; - } - - const queryRunner = this.coreDataSource.createQueryRunner(); - - await queryRunner.connect(); - await queryRunner.startTransaction(); - - try { - await queryRunner.query( - `ALTER TABLE "core"."file" ADD CONSTRAINT "IDX_APPLICATION_PATH_WORKSPACE_ID_APPLICATION_ID_UNIQUE" UNIQUE ("workspaceId", "applicationId", "path")`, - ); - - await queryRunner.commitTransaction(); - this.logger.log('Successfully added file entity unique constraint'); - } catch (error) { - await queryRunner.rollbackTransaction(); - this.logger.error( - `Rolling back addFileEntityUniqueConstraint: ${error.message}`, - ); - } finally { - await queryRunner.release(); - } - } -} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-17/1-17-fix-morph-relation-field-names.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-17/1-17-fix-morph-relation-field-names.command.ts deleted file mode 100644 index 8d96f99689..0000000000 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-17/1-17-fix-morph-relation-field-names.command.ts +++ /dev/null @@ -1,337 +0,0 @@ -import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; - -import { Command } from 'nest-commander'; -import { FieldMetadataType } from 'twenty-shared/types'; -import { capitalize } from 'twenty-shared/utils'; -import { DataSource, QueryFailedError, Repository } from 'typeorm'; - -import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner'; -import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner'; -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'; -import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service'; -import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; -import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; -import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; - -interface MismatchedField { - fieldId: string; - currentFieldName: string; - expectedFieldName: string; - sourceObjectName: string; - targetObjectNameSingular: string; - currentJoinColumnName: string; - expectedJoinColumnName: string; - workspaceSchema: string; -} - -@Command({ - name: 'upgrade:1-17:fix-morph-relation-field-names', - description: - 'Fix morph relation field names that do not match their target object names', -}) -export class FixMorphRelationFieldNamesCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner { - constructor( - @InjectRepository(WorkspaceEntity) - protected readonly workspaceRepository: Repository, - @InjectDataSource() - private readonly coreDataSource: DataSource, - private readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager, - protected readonly dataSourceService: DataSourceService, - private readonly workspaceCacheService: WorkspaceCacheService, - private readonly workspaceCacheStorageService: WorkspaceCacheStorageService, - private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService, - ) { - super(workspaceRepository, twentyORMGlobalManager, dataSourceService); - } - - override async runOnWorkspace({ - workspaceId, - options, - }: RunOnWorkspaceArgs): Promise { - const dryRun = options?.dryRun ?? false; - - this.logger.log( - `${dryRun ? '[DRY RUN] ' : ''}Checking morph relation field names for workspace ${workspaceId}`, - ); - - const mismatchedFields = await this.findMismatchedFields(workspaceId); - - if (mismatchedFields.length === 0) { - this.logger.log( - `✅ No mismatched fields found for workspace ${workspaceId}`, - ); - - return; - } - - this.logger.log( - `Found ${mismatchedFields.length} mismatched field(s) in workspace ${workspaceId}:`, - ); - - for (const field of mismatchedFields) { - this.logger.log( - ` - ${field.sourceObjectName}.${field.currentFieldName} → ${field.expectedFieldName} (target: ${field.targetObjectNameSingular})`, - ); - } - - if (dryRun) { - this.logger.log('[DRY RUN] No changes made'); - this.logger.log( - '⚠️ Before running without --dry-run, please ensure you have a database backup', - ); - - return; - } - - this.logger.log( - '⚠️ Starting fix. Ensure you have a database backup before proceeding.', - ); - - const queryRunner = this.coreDataSource.createQueryRunner(); - - await queryRunner.connect(); - await queryRunner.startTransaction(); - - try { - let fixedCount = 0; - - for (const field of mismatchedFields) { - // Step 1: Attempt to rename the column, handling failures gracefully - if (field.currentJoinColumnName !== field.expectedJoinColumnName) { - // Use a savepoint so a failed rename doesn't abort the whole transaction - const renameColumnSavepoint = 'rename_column'; - - await queryRunner.query(`SAVEPOINT ${renameColumnSavepoint}`); - try { - await queryRunner.query( - `ALTER TABLE "${field.workspaceSchema}"."${field.sourceObjectName}" - RENAME COLUMN "${field.currentJoinColumnName}" TO "${field.expectedJoinColumnName}"`, - ); - await queryRunner.query( - `RELEASE SAVEPOINT ${renameColumnSavepoint}`, - ); - this.logger.log( - `Renamed column ${field.currentJoinColumnName} → ${field.expectedJoinColumnName} in ${field.workspaceSchema}.${field.sourceObjectName}`, - ); - } catch (error) { - await queryRunner.query( - `ROLLBACK TO SAVEPOINT ${renameColumnSavepoint}`, - ); - await queryRunner.query( - `RELEASE SAVEPOINT ${renameColumnSavepoint}`, - ); - const errorCode = this.getPostgresErrorCode(error); - - if (errorCode === '42701') { - this.logger.warn( - `⚠️ SKIPPING ${field.sourceObjectName}.${field.currentFieldName}: Both columns exist (${field.currentJoinColumnName} and ${field.expectedJoinColumnName}). Manual intervention required.`, - ); - continue; - } - if (errorCode !== '42703') { - throw error; - } - this.logger.log( - `Column rename skipped for ${field.sourceObjectName}.${field.currentJoinColumnName} (column may already be renamed or missing), updating metadata only`, - ); - } - } - - // Step 2: Verify field still exists with expected current name before updating - const fieldCheck = await queryRunner.query( - `SELECT id, name FROM core."fieldMetadata" WHERE id = $1`, - [field.fieldId], - ); - - if (fieldCheck.length === 0) { - this.logger.warn( - `⚠️ Field ${field.fieldId} no longer exists, skipping`, - ); - continue; - } - - if (fieldCheck[0].name !== field.currentFieldName) { - this.logger.warn( - `⚠️ Field ${field.fieldId} name changed from ${field.currentFieldName} to ${fieldCheck[0].name} since query, skipping`, - ); - continue; - } - - // Step 3: Update field metadata - const newSettings = { - joinColumnName: field.expectedJoinColumnName, - }; - - const updateResult = await queryRunner.query( - `UPDATE core."fieldMetadata" - SET name = $1, - settings = settings || $2::jsonb - WHERE id = $3 - AND name = $4 - RETURNING id`, - [ - field.expectedFieldName, - JSON.stringify(newSettings), - field.fieldId, - field.currentFieldName, - ], - ); - - if (updateResult.length > 0) { - this.logger.log( - `✓ Updated fieldMetadata: ${field.currentFieldName} → ${field.expectedFieldName}`, - ); - fixedCount++; - } else { - this.logger.log( - `Field metadata already up to date for ${field.currentFieldName}`, - ); - } - } - - await queryRunner.commitTransaction(); - - if (fixedCount > 0) { - // Invalidate flat entity caches (new cache system) - await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [ - 'flatFieldMetadataMaps', - 'ORMEntityMetadatas', - 'rolesPermissions', - 'userWorkspaceRoleMap', - 'flatRoleTargetMaps', - 'apiKeyRoleMap', - 'flatRoleTargetByAgentIdMaps', - ]); - - // Increment metadata version so GraphQL schema is regenerated - await this.workspaceMetadataVersionService.incrementMetadataVersion( - workspaceId, - ); - - // Flush legacy cache (GraphQL typeDefs, ORM entity schemas, GraphQL operations) - await this.workspaceCacheStorageService.flush(workspaceId); - - this.logger.log(`Cache invalidated and metadata version incremented`); - } - - this.logger.log( - `✅ Successfully fixed ${fixedCount} field(s) in workspace ${workspaceId}`, - ); - } catch (error) { - if (queryRunner.isTransactionActive) { - await queryRunner.rollbackTransaction(); - this.logger.error( - `Error fixing morph relation field names (rolled transaction back on ${workspaceId})`, - error, - ); - } else { - this.logger.error( - `Error fixing morph relation field names after commit on ${workspaceId}`, - error, - ); - } - throw error; - } finally { - await queryRunner.release(); - } - } - - private async findMismatchedFields( - workspaceId: string, - ): Promise { - // Only process fields on these specific objects that use the target morph pattern - const allowedSourceObjects = [ - 'noteTarget', - 'taskTarget', - 'attachment', - 'timelineActivity', - ]; - - const result = await this.coreDataSource.query( - `SELECT - fm.id AS "fieldId", - fm.name AS "currentFieldName", - fm.settings->>'joinColumnName' AS "currentJoinColumnName", - source_om."nameSingular" AS "sourceObjectName", - target_om."nameSingular" AS "targetObjectNameSingular", - ds.schema AS "workspaceSchema" - FROM core."fieldMetadata" fm - JOIN core."objectMetadata" source_om ON fm."objectMetadataId" = source_om.id - JOIN core."objectMetadata" target_om ON fm."relationTargetObjectMetadataId" = target_om.id - JOIN core."dataSource" ds ON ds."workspaceId" = fm."workspaceId" - WHERE fm."workspaceId" = $1 - AND fm.type = $2 - AND fm.name LIKE 'target%' - AND source_om."nameSingular" = ANY($3)`, - [workspaceId, FieldMetadataType.MORPH_RELATION, allowedSourceObjects], - ); - - // Filter in JavaScript to handle camelCase properly - return result - .filter( - (row: { - currentFieldName: string; - targetObjectNameSingular: string; - }) => { - const expectedFieldName = `target${capitalize(row.targetObjectNameSingular)}`; - - return row.currentFieldName !== expectedFieldName; - }, - ) - .map( - (row: { - fieldId: string; - currentFieldName: string; - currentJoinColumnName: string | null; - sourceObjectName: string; - targetObjectNameSingular: string; - workspaceSchema: string; - }) => { - const expectedFieldName = `target${capitalize(row.targetObjectNameSingular)}`; - const expectedJoinColumnName = - computeMorphOrRelationFieldJoinColumnName({ - name: expectedFieldName, - }); - - // Fallback: if settings.joinColumnName is null, derive from field name - const currentJoinColumnName = - row.currentJoinColumnName ?? - computeMorphOrRelationFieldJoinColumnName({ - name: row.currentFieldName, - }); - - return { - fieldId: row.fieldId, - currentFieldName: row.currentFieldName, - expectedFieldName, - sourceObjectName: row.sourceObjectName, - targetObjectNameSingular: row.targetObjectNameSingular, - currentJoinColumnName, - expectedJoinColumnName, - workspaceSchema: row.workspaceSchema, - }; - }, - ); - } - - private getPostgresErrorCode(error: unknown): string | null { - if (!(error instanceof QueryFailedError)) { - return null; - } - const driverError: unknown = error.driverError; - - if ( - typeof driverError === 'object' && - driverError !== null && - 'code' in driverError - ) { - const code = (driverError as { code?: unknown }).code; - - return typeof code === 'string' ? code : null; - } - - return null; - } -} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-17/1-17-identify-webhook-metadata.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-17/1-17-identify-webhook-metadata.command.ts deleted file mode 100644 index 686d47a417..0000000000 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-17/1-17-identify-webhook-metadata.command.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; - -import { Command } from 'nest-commander'; -import { DataSource, IsNull, Repository } from 'typeorm'; -import { v4 } from 'uuid'; - -import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner'; -import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner'; -import { ApplicationService } from 'src/engine/core-modules/application/application.service'; -import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; -import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; -import { getMetadataFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-flat-entity-maps-key.util'; -import { getMetadataRelatedMetadataNames } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-related-metadata-names.util'; -import { WebhookEntity } from 'src/engine/metadata-modules/webhook/entities/webhook.entity'; -import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; -import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; - -@Command({ - name: 'upgrade:1-17:identify-webhook-metadata', - description: - 'Identify webhook metadata (backfill universalIdentifier and applicationId)', -}) -export class IdentifyWebhookMetadataCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner { - constructor( - @InjectRepository(WorkspaceEntity) - protected readonly workspaceRepository: Repository, - protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager, - protected readonly dataSourceService: DataSourceService, - protected readonly applicationService: ApplicationService, - protected readonly workspaceCacheService: WorkspaceCacheService, - @InjectDataSource() - private readonly coreDataSource: DataSource, - ) { - super(workspaceRepository, twentyORMGlobalManager, dataSourceService); - } - - override async runOnWorkspace({ - workspaceId, - options, - }: RunOnWorkspaceArgs): Promise { - this.logger.log( - `Running identify webhook metadata for workspace ${workspaceId}`, - ); - - const { workspaceCustomFlatApplication } = - await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow( - { workspaceId }, - ); - - await this.identifyWebhookEntities({ - workspaceId, - workspaceCustomApplicationId: workspaceCustomFlatApplication.id, - dryRun: options.dryRun ?? false, - }); - } - - private async identifyWebhookEntities({ - workspaceId, - workspaceCustomApplicationId, - dryRun, - }: { - workspaceId: string; - workspaceCustomApplicationId: string; - dryRun: boolean; - }): Promise { - const webhookRepository = this.coreDataSource.getRepository(WebhookEntity); - - const webhooksWithoutApplicationId = await webhookRepository.find({ - select: ['id', 'universalIdentifier', 'applicationId'], - where: { - workspaceId, - applicationId: IsNull(), - }, - withDeleted: true, - }); - - if (webhooksWithoutApplicationId.length === 0) { - this.logger.log( - `No webhook entities found without applicationId for workspace ${workspaceId}`, - ); - - return; - } - - const updates = webhooksWithoutApplicationId.map((webhook) => ({ - id: webhook.id, - universalIdentifier: webhook.universalIdentifier ?? v4(), - applicationId: workspaceCustomApplicationId, - })); - - this.logger.log( - `Found ${updates.length} webhook entities to update for workspace ${workspaceId}`, - ); - - if (!dryRun) { - await webhookRepository.save(updates); - } - - const relatedMetadataNames = getMetadataRelatedMetadataNames('webhook'); - const relatedCacheKeysToInvalidate = relatedMetadataNames.map( - getMetadataFlatEntityMapsKey, - ); - - const flatEntityMapsKey = getMetadataFlatEntityMapsKey('webhook'); - - this.logger.log( - `Invalidating caches: ${flatEntityMapsKey} ${relatedCacheKeysToInvalidate.join(' ')}`, - ); - - if (!dryRun) { - await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [ - flatEntityMapsKey, - ...relatedCacheKeysToInvalidate, - ]); - } - } -} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-17/1-17-make-webhook-universal-identifier-and-application-id-not-nullable-migration.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-17/1-17-make-webhook-universal-identifier-and-application-id-not-nullable-migration.command.ts deleted file mode 100644 index 8a6ece7a20..0000000000 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-17/1-17-make-webhook-universal-identifier-and-application-id-not-nullable-migration.command.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; - -import { Command } from 'nest-commander'; -import { DataSource, Repository } from 'typeorm'; - -import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner'; -import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner'; -import { makeWebhookUniversalIdentifierAndApplicationIdNotNullQueries } from 'src/database/typeorm/core/migrations/utils/1769525557511-makeWebhookUniversalIdentifierAndApplicationIdNotNull.util'; -import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; -import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; -import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; - -@Command({ - name: 'upgrade:1-17:make-webhook-universal-identifier-and-application-id-not-nullable-migration', - description: - 'Make universalIdentifier and applicationId columns NOT NULL on webhook entity', -}) -export class MakeWebhookUniversalIdentifierAndApplicationIdNotNullableMigrationCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner { - private hasRunOnce = false; - - constructor( - @InjectRepository(WorkspaceEntity) - protected readonly workspaceRepository: Repository, - protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager, - protected readonly dataSourceService: DataSourceService, - @InjectDataSource() - private readonly coreDataSource: DataSource, - ) { - super(workspaceRepository, twentyORMGlobalManager, dataSourceService); - } - - override async runOnWorkspace({ - options, - }: RunOnWorkspaceArgs): Promise { - if (this.hasRunOnce) { - this.logger.warn( - 'Skipping has already been run once MakeWebhookUniversalIdentifierAndApplicationIdNotNullableMigrationCommand', - ); - - return; - } - - if (options.dryRun) { - return; - } - - const queryRunner = this.coreDataSource.createQueryRunner(); - - await queryRunner.connect(); - await queryRunner.startTransaction(); - - try { - await makeWebhookUniversalIdentifierAndApplicationIdNotNullQueries( - queryRunner, - ); - - await queryRunner.commitTransaction(); - this.logger.log( - 'Successfully run MakeWebhookUniversalIdentifierAndApplicationIdNotNullableMigrationCommand', - ); - this.hasRunOnce = true; - } catch (error) { - await queryRunner.rollbackTransaction(); - this.logger.error( - `Rolling back MakeWebhookUniversalIdentifierAndApplicationIdNotNullableMigrationCommand: ${error.message}`, - ); - } finally { - await queryRunner.release(); - } - } -} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-17/1-17-migrate-attachment-to-morph-relations.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-17/1-17-migrate-attachment-to-morph-relations.command.ts deleted file mode 100644 index e0c8dfb9de..0000000000 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-17/1-17-migrate-attachment-to-morph-relations.command.ts +++ /dev/null @@ -1,334 +0,0 @@ -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'; -import { capitalize } from 'twenty-shared/utils'; -import { DataSource, Repository } from 'typeorm'; - -import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner'; -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'; -import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; -import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.util'; -import { getMetadataFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-flat-entity-maps-key.util'; -import { getMetadataRelatedMetadataNames } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-related-metadata-names.util'; -import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; -import { isMorphOrRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util'; -import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; -import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service'; -import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; -import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; -import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; -import { type WorkspaceCacheKeyName } from 'src/engine/workspace-cache/types/workspace-cache-key.type'; -import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util'; -import { TWENTY_STANDARD_APPLICATION } from 'src/engine/workspace-manager/twenty-standard-application/constants/twenty-standard-applications'; - -type RelationFieldMetadataSettings = - FieldMetadataSettings; - -@Command({ - name: 'upgrade:1-17:migrate-attachment-to-morph-relations', - description: - 'Migrate attachment relations to morph relation fields and set feature flag', -}) -export class MigrateAttachmentToMorphRelationsCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner { - constructor( - @InjectRepository(WorkspaceEntity) - protected readonly workspaceRepository: Repository, - private readonly featureFlagService: FeatureFlagService, - @InjectDataSource() - private readonly coreDataSource: DataSource, - private readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager, - protected readonly dataSourceService: DataSourceService, - private readonly workspaceCacheStorageService: WorkspaceCacheStorageService, - private readonly workspaceCacheService: WorkspaceCacheService, - private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService, - ) { - super(workspaceRepository, twentyORMGlobalManager, dataSourceService); - } - - override async runOnWorkspace({ - workspaceId, - options, - }: RunOnWorkspaceArgs): Promise { - 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...`, - ); - - 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([ - 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({ - 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 => - 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( - `Column "${oldJoinColumnName}" already renamed. Skipping...`, - ); - continue; - } - - try { - await queryRunner.query( - `ALTER TABLE "${schemaName}"."${tableName}" - RENAME COLUMN "${oldJoinColumnName}" TO "${newJoinColumnName}"`, - ); - this.logger.log( - `Renamed column "${oldJoinColumnName}" to "${newJoinColumnName}" for "${tableName}"`, - ); - } catch (error) { - this.logger.error( - `Error renaming column "${oldJoinColumnName}" to "${newJoinColumnName}" for "${tableName}" in workspace ${workspaceId}`, - error, - ); - - throw error; - } - } - - this.logger.log(`✅ Successfully migrated attachment records`); - - const morphId = - STANDARD_OBJECTS.attachment.morphIds.targetMorphId.morphId; - - for (const { - field: fieldToMigrate, - newFieldName, - newJoinColumnName, - } of fieldMigrations) { - const settings = { - ...fieldToMigrate.settings, - joinColumnName: newJoinColumnName, - }; - - try { - const result = await queryRunner.query( - `UPDATE core."fieldMetadata" - SET name = $1, type = $5, "morphId" = $3, settings = $4 - WHERE id = $2`, - [ - newFieldName, - fieldToMigrate.id, - morphId, - settings, - FieldMetadataType.MORPH_RELATION, - ], - ); - - const rowsUpdated = result[1] || 0; - - if (rowsUpdated > 0) { - this.logger.log( - `Updated fieldMetadata: ${fieldToMigrate.name} → ${newFieldName} (type: MORPH_RELATION)`, - ); - } - } catch (error) { - this.logger.error( - `Error updating fieldMetadata for field "${fieldToMigrate.name}" in workspace ${workspaceId}`, - error, - ); - - throw error; - } - } - - this.logger.log(`✅ Successfully migrated attachment fieldmetadata`); - - await queryRunner.commitTransaction(); - - await this.featureFlagService.enableFeatureFlags( - [FeatureFlagKey.IS_ATTACHMENT_MIGRATED], - workspaceId, - ); - - const relatedMetadataNames = - getMetadataRelatedMetadataNames('fieldMetadata'); - const relatedCacheKeysToInvalidate: WorkspaceCacheKeyName[] = - relatedMetadataNames.map(getMetadataFlatEntityMapsKey); - const cacheKeysToInvalidate: WorkspaceCacheKeyName[] = [ - 'flatFieldMetadataMaps', - ...relatedCacheKeysToInvalidate, - 'featureFlagsMap', - ]; - - this.logger.log( - `Invalidating caches: ${cacheKeysToInvalidate.join(' ')}`, - ); - - await this.workspaceCacheService.invalidateAndRecompute( - workspaceId, - cacheKeysToInvalidate, - ); - - await this.workspaceMetadataVersionService.incrementMetadataVersion( - workspaceId, - ); - 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) { - if (queryRunner.isTransactionActive) { - await queryRunner.rollbackTransaction(); - this.logger.error( - `Error migrating attachment to morph relations (rolled transaction back on ${workspaceId})`, - error, - ); - } else { - this.logger.error( - `Error migrating attachment to morph relations after commit on ${workspaceId}`, - error, - ); - } - throw error; - } finally { - await queryRunner.release(); - } - } - - private isTwentyStandardApplicationField({ - field, - flatApplicationMaps, - workspaceId, - }: { - field: { id: string; name: string; applicationId: string }; - flatApplicationMaps: FlatApplicationCacheMaps; - workspaceId: string; - }): boolean { - const application = flatApplicationMaps.byId[field.applicationId]; - - if (!application) { - this.logger.error( - `🟥 Application not found for field "${field.name}" (${field.id}) in workspace ${workspaceId} (applicationId: ${field.applicationId})`, - ); - - return false; - } - - return ( - application.universalIdentifier === - TWENTY_STANDARD_APPLICATION.universalIdentifier - ); - } -} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-17/1-17-migrate-date-time-is-filter-values.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-17/1-17-migrate-date-time-is-filter-values.command.ts deleted file mode 100644 index f4b1748f46..0000000000 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-17/1-17-migrate-date-time-is-filter-values.command.ts +++ /dev/null @@ -1,157 +0,0 @@ -import { Logger } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; - -import { Command } from 'nest-commander'; -import { - FieldMetadataType, - ViewFilterOperand, - FeatureFlagKey, -} from 'twenty-shared/types'; -import { Repository } from 'typeorm'; - -import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner'; -import { type RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner'; -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 { ViewFilterEntity } from 'src/engine/metadata-modules/view-filter/entities/view-filter.entity'; -import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service'; -import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; -import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; -import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; -import { WorkspaceCacheKeyName } from 'src/engine/workspace-cache/types/workspace-cache-key.type'; - -@Command({ - name: 'upgrade:1-17:migrate-date-time-is-filter-values', - description: 'Migrate DATE_TIME IS operand values from Instant to Plain Date', -}) -export class MigrateDateTimeIsFilterValuesCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner { - protected readonly logger = new Logger( - MigrateDateTimeIsFilterValuesCommand.name, - {}, - ); - - constructor( - @InjectRepository(WorkspaceEntity) - protected readonly workspaceRepository: Repository, - protected readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, - protected readonly dataSourceService: DataSourceService, - @InjectRepository(ViewFilterEntity) - private readonly viewFilterRepository: Repository, - private readonly featureFlagService: FeatureFlagService, - private readonly workspaceCacheService: WorkspaceCacheService, - private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService, - private readonly workspaceCacheStorageService: WorkspaceCacheStorageService, - ) { - super(workspaceRepository, globalWorkspaceOrmManager, dataSourceService); - } - - override async runOnWorkspace({ - workspaceId, - options, - }: RunOnWorkspaceArgs): Promise { - const isDryRun = options.dryRun || false; - - this.logger.log(`Processing workspace ${workspaceId}`); - - const viewFilters = await this.viewFilterRepository.find({ - where: { - workspaceId, - }, - relations: ['fieldMetadata'], - withDeleted: true, - }); - - const filtersToUpdate = viewFilters.filter((filter) => { - if ( - !filter.fieldMetadata || - filter.fieldMetadata.type !== FieldMetadataType.DATE_TIME || - filter.operand !== ViewFilterOperand.IS - ) { - return false; - } - - const value = filter.value; - - if (typeof value !== 'string') { - return false; - } - - return value.includes('T'); - }); - - if (filtersToUpdate.length === 0) { - return; - } - - this.logger.log( - `Found ${filtersToUpdate.length} filters to migrate in workspace ${workspaceId}`, - ); - - if (isDryRun) { - this.logger.log( - `Dry run: would update ${filtersToUpdate.length} filters`, - ); - - return; - } - - let updatedCount = 0; - - for (const filter of filtersToUpdate) { - try { - const newDate = (filter.value as string).split('T')[0]; - - if (!/^\d{4}-\d{2}-\d{2}$/.test(newDate)) { - this.logger.warn( - `Skipping invalid date extraction for filter ${filter.id}: ${filter.value} -> ${newDate}`, - ); - continue; - } - - await this.viewFilterRepository.update(filter.id, { - value: newDate, - }); - updatedCount++; - } catch (err) { - this.logger.error( - `Failed to migrate filter ${filter.id}: ${err instanceof Error ? err.message : String(err)}`, - ); - } - } - - this.logger.log( - `Migrated ${updatedCount} filters in workspace ${workspaceId}`, - ); - - await this.featureFlagService.enableFeatureFlags( - [FeatureFlagKey.IS_DATE_TIME_WHOLE_DAY_FILTER_ENABLED], - workspaceId, - ); - - this.logger.log( - `Enabled IS_DATE_TIME_WHOLE_DAY_FILTER_ENABLED for workspace ${workspaceId}`, - ); - - const cacheKeysToInvalidate: WorkspaceCacheKeyName[] = [ - 'flatViewFilterMaps', - ]; - - this.logger.log(`Invalidating caches: ${cacheKeysToInvalidate.join(' ')}`); - - await this.workspaceCacheService.invalidateAndRecompute( - workspaceId, - cacheKeysToInvalidate, - ); - - await this.workspaceMetadataVersionService.incrementMetadataVersion( - workspaceId, - ); - - this.logger.log(`Cache flushed`); - - this.logger.log(`Flush cache for workspace ${workspaceId}`); - - await this.workspaceCacheStorageService.flush(workspaceId); - } -} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-17/1-17-migrate-note-target-to-morph-relations.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-17/1-17-migrate-note-target-to-morph-relations.command.ts deleted file mode 100644 index 1be212083a..0000000000 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-17/1-17-migrate-note-target-to-morph-relations.command.ts +++ /dev/null @@ -1,331 +0,0 @@ -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'; -import { capitalize } from 'twenty-shared/utils'; -import { DataSource, Repository } from 'typeorm'; - -import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner'; -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'; -import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util'; -import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.util'; -import { getMetadataFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-flat-entity-maps-key.util'; -import { getMetadataRelatedMetadataNames } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-related-metadata-names.util'; -import { isMorphOrRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util'; -import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; -import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service'; -import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; -import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; -import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; -import { type WorkspaceCacheKeyName } from 'src/engine/workspace-cache/types/workspace-cache-key.type'; -import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util'; -import { TWENTY_STANDARD_APPLICATION } from 'src/engine/workspace-manager/twenty-standard-application/constants/twenty-standard-applications'; - -type RelationFieldMetadataSettings = - FieldMetadataSettings; - -@Command({ - name: 'upgrade:1-17:migrate-note-target-to-morph-relations', - description: - 'Migrate noteTarget relations to morph relation fields and set feature flag', -}) -export class MigrateNoteTargetToMorphRelationsCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner { - constructor( - @InjectRepository(WorkspaceEntity) - protected readonly workspaceRepository: Repository, - private readonly featureFlagService: FeatureFlagService, - @InjectDataSource() - private readonly coreDataSource: DataSource, - private readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager, - protected readonly dataSourceService: DataSourceService, - private readonly workspaceCacheStorageService: WorkspaceCacheStorageService, - private readonly workspaceCacheService: WorkspaceCacheService, - private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService, - ) { - super(workspaceRepository, twentyORMGlobalManager, dataSourceService); - } - - override async runOnWorkspace({ - workspaceId, - options, - }: RunOnWorkspaceArgs): Promise { - 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...`, - ); - - 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([ - STANDARD_OBJECTS.noteTarget.fields.targetPerson.universalIdentifier, - STANDARD_OBJECTS.noteTarget.fields.targetCompany.universalIdentifier, - STANDARD_OBJECTS.noteTarget.fields.targetOpportunity - .universalIdentifier, - ]); - - const noteTargetObjectMetadata = - findFlatEntityByUniversalIdentifier({ - 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) { - this.logger.log( - `Column "${oldJoinColumnName}" already renamed. Skipping...`, - ); - continue; - } - - try { - await queryRunner.query( - `ALTER TABLE "${schemaName}"."${tableName}" - RENAME COLUMN "${oldJoinColumnName}" TO "${newJoinColumnName}"`, - ); - this.logger.log( - `Renamed column "${oldJoinColumnName}" to "${newJoinColumnName}" for "${tableName}"`, - ); - } catch (error) { - this.logger.error( - `Error renaming column "${oldJoinColumnName}" to "${newJoinColumnName}" for "${tableName}" in workspace ${workspaceId}`, - error, - ); - - throw error; - } - } - - this.logger.log(`✅ Successfully migrated ${tableName} records`); - - const morphId = - STANDARD_OBJECTS.noteTarget.morphIds.targetMorphId.morphId; - - // Update field metadata - for (const { - field: fieldToMigrate, - newFieldName, - newFieldLabel, - newJoinColumnName, - } of fieldMigrations) { - const settings = { - ...fieldToMigrate.settings, - joinColumnName: newJoinColumnName, - }; - - try { - const result = await queryRunner.query( - `UPDATE core."fieldMetadata" - SET name = $1, type = $5, "morphId" = $3, settings = $4, "isActive" = true, label = $6 - WHERE id = $2`, - [ - newFieldName, - fieldToMigrate.id, - morphId, - settings, - FieldMetadataType.MORPH_RELATION, - newFieldLabel, - ], - ); - - const rowsUpdated = result[1] || 0; - - if (rowsUpdated > 0) { - this.logger.log( - `Updated fieldMetadata: ${fieldToMigrate.name} → ${newFieldName} (label: ${newFieldLabel}, type: MORPH_RELATION)`, - ); - } - } catch (error) { - this.logger.error( - `Error updating fieldMetadata for field "${fieldToMigrate.name}" in workspace ${workspaceId}`, - error, - ); - - throw error; - } - } - - this.logger.log(`✅ Successfully migrated ${tableName} fieldmetadata`); - - await queryRunner.commitTransaction(); - - await this.featureFlagService.enableFeatureFlags( - [FeatureFlagKey.IS_NOTE_TARGET_MIGRATED], - workspaceId, - ); - - const relatedMetadataNames = - getMetadataRelatedMetadataNames('fieldMetadata'); - const relatedCacheKeysToInvalidate: WorkspaceCacheKeyName[] = - relatedMetadataNames.map(getMetadataFlatEntityMapsKey); - const cacheKeysToInvalidate: WorkspaceCacheKeyName[] = [ - 'flatFieldMetadataMaps', - ...relatedCacheKeysToInvalidate, - 'featureFlagsMap', - ]; - - this.logger.log( - `Invalidating caches: ${cacheKeysToInvalidate.join(' ')}`, - ); - - await this.workspaceCacheService.invalidateAndRecompute( - workspaceId, - cacheKeysToInvalidate, - ); - - await this.workspaceMetadataVersionService.incrementMetadataVersion( - workspaceId, - ); - 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) { - if (queryRunner.isTransactionActive) { - await queryRunner.rollbackTransaction(); - this.logger.error( - `Error migrating noteTarget to morph relations (rolled transaction back on ${workspaceId})`, - error, - ); - } else { - this.logger.error( - `Error migrating noteTarget to morph relations after commit on ${workspaceId}`, - error, - ); - } - throw error; - } finally { - await queryRunner.release(); - } - } - - private isTwentyStandardApplicationField({ - field, - flatApplicationMaps, - workspaceId, - }: { - field: { id: string; name: string; applicationId: string }; - flatApplicationMaps: FlatApplicationCacheMaps; - workspaceId: string; - }): boolean { - const application = flatApplicationMaps.byId[field.applicationId]; - - if (!application) { - this.logger.error( - `🟥 Application not found for field "${field.name}" (${field.id}) in workspace ${workspaceId} (applicationId: ${field.applicationId})`, - ); - - return false; - } - - return ( - application.universalIdentifier === - TWENTY_STANDARD_APPLICATION.universalIdentifier - ); - } -} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-17/1-17-migrate-send-email-recipients.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-17/1-17-migrate-send-email-recipients.command.ts deleted file mode 100644 index aef554d5bc..0000000000 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-17/1-17-migrate-send-email-recipients.command.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { Logger } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; - -import { Command } from 'nest-commander'; -import { Repository } from 'typeorm'; - -import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner'; -import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner'; -import { migrateWorkflowSteps } from 'src/database/commands/upgrade-version-command/1-17/utils/migrate-send-email-step.util'; -import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; -import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; -import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; - -@Command({ - name: 'upgrade:1-17:migrate-send-email-recipients', - description: - 'Migrate send email action from legacy email field to recipients object', -}) -export class MigrateSendEmailRecipientsCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner { - protected readonly logger = new Logger( - MigrateSendEmailRecipientsCommand.name, - ); - - constructor( - @InjectRepository(WorkspaceEntity) - protected readonly workspaceRepository: Repository, - protected readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, - protected readonly dataSourceService: DataSourceService, - ) { - super(workspaceRepository, globalWorkspaceOrmManager, dataSourceService); - } - - override async runOnWorkspace({ - workspaceId, - options, - }: RunOnWorkspaceArgs): Promise { - const isDryRun = options.dryRun ?? false; - - this.logger.log( - `Running MigrateSendEmailRecipientsCommand for workspace ${workspaceId}`, - ); - - const workflowVersionRepository = - await this.globalWorkspaceOrmManager.getRepository( - workspaceId, - 'workflowVersion', - { shouldBypassPermissionChecks: true }, - ); - - const workflowVersions = await workflowVersionRepository.find({ - select: ['id', 'steps'], - }); - - let migratedCount = 0; - - for (const version of workflowVersions) { - const { migratedSteps, hasChanges } = migrateWorkflowSteps(version.steps); - - if (!hasChanges) { - continue; - } - - migratedCount++; - - if (isDryRun) { - this.logger.log( - `[DRY RUN] Would migrate workflow version ${version.id} in workspace ${workspaceId}`, - ); - } else { - await workflowVersionRepository.update( - { id: version.id }, - { steps: migratedSteps }, - ); - - this.logger.log( - `Migrated workflow version ${version.id} in workspace ${workspaceId}`, - ); - } - } - - this.logger.log( - `${isDryRun ? '[DRY RUN] Would have migrated' : 'Migrated'} ${migratedCount} workflow version(s) in workspace ${workspaceId}`, - ); - } -} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-17/1-17-migrate-task-target-to-morph-relations.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-17/1-17-migrate-task-target-to-morph-relations.command.ts deleted file mode 100644 index abdcb0bdb8..0000000000 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-17/1-17-migrate-task-target-to-morph-relations.command.ts +++ /dev/null @@ -1,331 +0,0 @@ -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'; -import { capitalize } from 'twenty-shared/utils'; -import { DataSource, Repository } from 'typeorm'; - -import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner'; -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'; -import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util'; -import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.util'; -import { getMetadataFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-flat-entity-maps-key.util'; -import { getMetadataRelatedMetadataNames } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-related-metadata-names.util'; -import { isMorphOrRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util'; -import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; -import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service'; -import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; -import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; -import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; -import { type WorkspaceCacheKeyName } from 'src/engine/workspace-cache/types/workspace-cache-key.type'; -import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util'; -import { TWENTY_STANDARD_APPLICATION } from 'src/engine/workspace-manager/twenty-standard-application/constants/twenty-standard-applications'; - -type RelationFieldMetadataSettings = - FieldMetadataSettings; - -@Command({ - name: 'upgrade:1-17:migrate-task-target-to-morph-relations', - description: - 'Migrate taskTarget relations to morph relation fields and set feature flag', -}) -export class MigrateTaskTargetToMorphRelationsCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner { - constructor( - @InjectRepository(WorkspaceEntity) - protected readonly workspaceRepository: Repository, - private readonly featureFlagService: FeatureFlagService, - @InjectDataSource() - private readonly coreDataSource: DataSource, - private readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager, - protected readonly dataSourceService: DataSourceService, - private readonly workspaceCacheStorageService: WorkspaceCacheStorageService, - private readonly workspaceCacheService: WorkspaceCacheService, - private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService, - ) { - super(workspaceRepository, twentyORMGlobalManager, dataSourceService); - } - - override async runOnWorkspace({ - workspaceId, - options, - }: RunOnWorkspaceArgs): Promise { - 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...`, - ); - - 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([ - STANDARD_OBJECTS.taskTarget.fields.targetPerson.universalIdentifier, - STANDARD_OBJECTS.taskTarget.fields.targetCompany.universalIdentifier, - STANDARD_OBJECTS.taskTarget.fields.targetOpportunity - .universalIdentifier, - ]); - - const taskTargetObjectMetadata = - findFlatEntityByUniversalIdentifier({ - 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) { - this.logger.log( - `Column "${oldJoinColumnName}" already renamed. Skipping...`, - ); - continue; - } - - try { - await queryRunner.query( - `ALTER TABLE "${schemaName}"."${tableName}" - RENAME COLUMN "${oldJoinColumnName}" TO "${newJoinColumnName}"`, - ); - this.logger.log( - `Renamed column "${oldJoinColumnName}" to "${newJoinColumnName}" for "${tableName}"`, - ); - } catch (error) { - this.logger.error( - `Error renaming column "${oldJoinColumnName}" to "${newJoinColumnName}" for "${tableName}" in workspace ${workspaceId}`, - error, - ); - - throw error; - } - } - - this.logger.log(`✅ Successfully migrated ${tableName} records`); - - const morphId = - STANDARD_OBJECTS.taskTarget.morphIds.targetMorphId.morphId; - - // Update field metadata - for (const { - field: fieldToMigrate, - newFieldName, - newFieldLabel, - newJoinColumnName, - } of fieldMigrations) { - const settings = { - ...fieldToMigrate.settings, - joinColumnName: newJoinColumnName, - }; - - try { - const result = await queryRunner.query( - `UPDATE core."fieldMetadata" - SET name = $1, type = $5, "morphId" = $3, settings = $4, "isActive" = true, label = $6 - WHERE id = $2`, - [ - newFieldName, - fieldToMigrate.id, - morphId, - settings, - FieldMetadataType.MORPH_RELATION, - newFieldLabel, - ], - ); - - const rowsUpdated = result[1] || 0; - - if (rowsUpdated > 0) { - this.logger.log( - `Updated fieldMetadata: ${fieldToMigrate.name} → ${newFieldName} (label: ${newFieldLabel}, type: MORPH_RELATION)`, - ); - } - } catch (error) { - this.logger.error( - `Error updating fieldMetadata for field "${fieldToMigrate.name}" in workspace ${workspaceId}`, - error, - ); - - throw error; - } - } - - this.logger.log(`✅ Successfully migrated ${tableName} fieldmetadata`); - - await queryRunner.commitTransaction(); - - await this.featureFlagService.enableFeatureFlags( - [FeatureFlagKey.IS_TASK_TARGET_MIGRATED], - workspaceId, - ); - - const relatedMetadataNames = - getMetadataRelatedMetadataNames('fieldMetadata'); - const relatedCacheKeysToInvalidate: WorkspaceCacheKeyName[] = - relatedMetadataNames.map(getMetadataFlatEntityMapsKey); - const cacheKeysToInvalidate: WorkspaceCacheKeyName[] = [ - 'flatFieldMetadataMaps', - ...relatedCacheKeysToInvalidate, - 'featureFlagsMap', - ]; - - this.logger.log( - `Invalidating caches: ${cacheKeysToInvalidate.join(' ')}`, - ); - - await this.workspaceCacheService.invalidateAndRecompute( - workspaceId, - cacheKeysToInvalidate, - ); - - await this.workspaceMetadataVersionService.incrementMetadataVersion( - workspaceId, - ); - 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) { - if (queryRunner.isTransactionActive) { - await queryRunner.rollbackTransaction(); - this.logger.error( - `Error migrating taskTarget to morph relations (rolled transaction back on ${workspaceId})`, - error, - ); - } else { - this.logger.error( - `Error migrating taskTarget to morph relations after commit on ${workspaceId}`, - error, - ); - } - throw error; - } finally { - await queryRunner.release(); - } - } - - private isTwentyStandardApplicationField({ - field, - flatApplicationMaps, - workspaceId, - }: { - field: { id: string; name: string; applicationId: string }; - flatApplicationMaps: FlatApplicationCacheMaps; - workspaceId: string; - }): boolean { - const application = flatApplicationMaps.byId[field.applicationId]; - - if (!application) { - this.logger.error( - `🟥 Application not found for field "${field.name}" (${field.id}) in workspace ${workspaceId} (applicationId: ${field.applicationId})`, - ); - - return false; - } - - return ( - application.universalIdentifier === - TWENTY_STANDARD_APPLICATION.universalIdentifier - ); - } -} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-17/1-17-upgrade-version-command.module.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-17/1-17-upgrade-version-command.module.ts deleted file mode 100644 index 27e7954bcc..0000000000 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-17/1-17-upgrade-version-command.module.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { Module } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; - -import { BackfillApplicationPackageFilesCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-backfill-application-package-files.command'; -import { DeleteFileRecordsAndUpdateTableCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-delete-all-files-and-update-table.command'; -import { FixMorphRelationFieldNamesCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-fix-morph-relation-field-names.command'; -import { IdentifyWebhookMetadataCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-identify-webhook-metadata.command'; -import { MakeWebhookUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-make-webhook-universal-identifier-and-application-id-not-nullable-migration.command'; -import { MigrateAttachmentToMorphRelationsCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-migrate-attachment-to-morph-relations.command'; -import { MigrateDateTimeIsFilterValuesCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-migrate-date-time-is-filter-values.command'; -import { MigrateNoteTargetToMorphRelationsCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-migrate-note-target-to-morph-relations.command'; -import { MigrateSendEmailRecipientsCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-migrate-send-email-recipients.command'; -import { MigrateTaskTargetToMorphRelationsCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-migrate-task-target-to-morph-relations.command'; -import { ApplicationModule } from 'src/engine/core-modules/application/application.module'; -import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; -import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; -import { FileStorageModule } from 'src/engine/core-modules/file-storage/file-storage.module'; -import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity'; -import { RecordPositionModule } from 'src/engine/core-modules/record-position/record-position.module'; -import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user-workspace.module'; -import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; -import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; -import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { FieldMetadataModule } from 'src/engine/metadata-modules/field-metadata/field-metadata.module'; -import { LogicFunctionEntity } from 'src/engine/metadata-modules/logic-function/logic-function.entity'; -import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module'; -import { ViewFilterEntity } from 'src/engine/metadata-modules/view-filter/entities/view-filter.entity'; -import { WebhookEntity } from 'src/engine/metadata-modules/webhook/entities/webhook.entity'; -import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module'; -import { GlobalWorkspaceDataSourceModule } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-datasource.module'; -import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module'; -import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module'; -import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module'; -import { AttachmentWorkspaceEntity } from 'src/modules/attachment/standard-objects/attachment.workspace-entity'; -import { NoteTargetWorkspaceEntity } from 'src/modules/note/standard-objects/note-target.workspace-entity'; -import { TaskTargetWorkspaceEntity } from 'src/modules/task/standard-objects/task-target.workspace-entity'; - -@Module({ - imports: [ - TypeOrmModule.forFeature([ - WorkspaceEntity, - ObjectMetadataEntity, - FieldMetadataEntity, - FeatureFlagEntity, - AttachmentWorkspaceEntity, - WebhookEntity, - NoteTargetWorkspaceEntity, - TaskTargetWorkspaceEntity, - FileEntity, - ViewFilterEntity, - LogicFunctionEntity, - ]), - DataSourceModule, - WorkspaceCacheStorageModule, - WorkspaceMetadataVersionModule, - FeatureFlagModule, - FileStorageModule.forRoot(), - WorkspaceCacheModule, - FieldMetadataModule, - ObjectMetadataModule, - ApplicationModule, - UserWorkspaceModule, - WorkspaceMigrationModule, - RecordPositionModule, - GlobalWorkspaceDataSourceModule, - ], - providers: [ - FixMorphRelationFieldNamesCommand, - MigrateAttachmentToMorphRelationsCommand, - MigrateNoteTargetToMorphRelationsCommand, - MigrateTaskTargetToMorphRelationsCommand, - IdentifyWebhookMetadataCommand, - MakeWebhookUniversalIdentifierAndApplicationIdNotNullableMigrationCommand, - DeleteFileRecordsAndUpdateTableCommand, - MigrateSendEmailRecipientsCommand, - MigrateDateTimeIsFilterValuesCommand, - BackfillApplicationPackageFilesCommand, - ], - exports: [ - FixMorphRelationFieldNamesCommand, - MigrateAttachmentToMorphRelationsCommand, - MigrateNoteTargetToMorphRelationsCommand, - MigrateTaskTargetToMorphRelationsCommand, - IdentifyWebhookMetadataCommand, - MakeWebhookUniversalIdentifierAndApplicationIdNotNullableMigrationCommand, - MigrateSendEmailRecipientsCommand, - MigrateDateTimeIsFilterValuesCommand, - DeleteFileRecordsAndUpdateTableCommand, - BackfillApplicationPackageFilesCommand, - ], -}) -export class V1_17_UpgradeVersionCommandModule {} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-17/utils/__tests__/migrate-send-email-step.util.spec.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-17/utils/__tests__/migrate-send-email-step.util.spec.ts deleted file mode 100644 index c4ccdc6c5a..0000000000 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-17/utils/__tests__/migrate-send-email-step.util.spec.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { - migrateInput, - migrateWorkflowSteps, - needsMigration, -} from 'src/database/commands/upgrade-version-command/1-17/utils/migrate-send-email-step.util'; - -const LEGACY_STEP_FROM_PRODUCTION = { - id: '3b8934cd-1dda-4acb-a050-785e04f7f40b', - name: 'Send Email', - type: 'SEND_EMAIL', - valid: false, - position: { x: 0, y: 150 }, - settings: { - input: { - body: '{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"sample"}]}]}', - email: 'sample@gmail.com', - files: [], - subject: 'sample', - connectedAccountId: '', - }, - outputSchema: {}, - errorHandlingOptions: { - retryOnFailure: { value: false }, - continueOnFailure: { value: false }, - }, - }, -}; - -describe('needsMigration', () => { - it('returns true for legacy email field', () => { - expect( - needsMigration({ connectedAccountId: '', email: 'test@example.com' }), - ).toBe(true); - }); - - it('returns false when recipients is already used', () => { - expect( - needsMigration({ - connectedAccountId: '', - recipients: { to: 'test@example.com' }, - }), - ).toBe(false); - }); -}); - -describe('migrateInput', () => { - it('converts legacy email to recipients.to', () => { - const result = migrateInput({ - connectedAccountId: 'acc-123', - email: 'legacy@example.com', - subject: 'Test', - body: 'Body', - }); - - expect(result.recipients.to).toBe('legacy@example.com'); - expect(result).not.toHaveProperty('email'); - }); -}); - -describe('migrateWorkflowSteps', () => { - it('migrates real production workflow with legacy email field', () => { - const { migratedSteps, hasChanges } = migrateWorkflowSteps([ - LEGACY_STEP_FROM_PRODUCTION, - ]); - - expect(hasChanges).toBe(true); - expect(migratedSteps[0].settings.input).toEqual({ - body: '{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"sample"}]}]}', - files: [], - subject: 'sample', - connectedAccountId: '', - recipients: { - to: 'sample@gmail.com', - cc: '', - bcc: '', - }, - }); - }); - - it('returns hasChanges false when no migration needed', () => { - const alreadyMigratedStep = { - ...LEGACY_STEP_FROM_PRODUCTION, - settings: { - ...LEGACY_STEP_FROM_PRODUCTION.settings, - input: { - connectedAccountId: '', - recipients: { to: 'new@example.com', cc: '', bcc: '' }, - subject: 'Test', - body: 'Body', - }, - }, - }; - - const { hasChanges } = migrateWorkflowSteps([alreadyMigratedStep]); - - expect(hasChanges).toBe(false); - }); -}); diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-17/utils/migrate-send-email-step.util.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-17/utils/migrate-send-email-step.util.ts deleted file mode 100644 index 22ea813a42..0000000000 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-17/utils/migrate-send-email-step.util.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { isDefined } from 'twenty-shared/utils'; - -import { WorkflowActionType } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type'; - -type LegacySendEmailInput = { - connectedAccountId: string; - email?: string; - recipients?: { - to?: string; - cc?: string; - bcc?: string; - }; - subject?: string; - body?: string; - files?: unknown[]; -}; - -type MigratedSendEmailInput = { - connectedAccountId: string; - recipients: { - to: string; - cc: string; - bcc: string; - }; - subject?: string; - body?: string; - files?: unknown[]; -}; - -type WorkflowStep = { - id: string; - type: string; - settings: { - input: LegacySendEmailInput | MigratedSendEmailInput; - }; -}; - -export const needsMigration = (input: LegacySendEmailInput): boolean => { - return isDefined(input.email); -}; - -export const migrateInput = ( - input: LegacySendEmailInput, -): MigratedSendEmailInput => { - const { email, recipients, ...rest } = input; - - const toValue = recipients?.to || email || ''; - const ccValue = recipients?.cc ?? ''; - const bccValue = recipients?.bcc ?? ''; - - return { - ...rest, - recipients: { - to: toValue, - cc: ccValue, - bcc: bccValue, - }, - }; -}; - -export const migrateWorkflowSteps = ( - steps: unknown, -): { migratedSteps: WorkflowStep[]; hasChanges: boolean } => { - if (!isDefined(steps) || !Array.isArray(steps) || steps.length === 0) { - return { migratedSteps: [], hasChanges: false }; - } - - const typedSteps = steps as WorkflowStep[]; - - let hasChanges = false; - - const migratedSteps = typedSteps.map((step) => { - if (step.type !== WorkflowActionType.SEND_EMAIL) { - return step; - } - - const input = step.settings.input as LegacySendEmailInput; - - if (!needsMigration(input)) { - return step; - } - - hasChanges = true; - - return { - ...step, - settings: { - ...step.settings, - input: migrateInput(input), - }, - }; - }); - - return { migratedSteps, hasChanges }; -}; diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-18/1-18-backfill-file-size-and-mime-type.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-18/1-18-backfill-file-size-and-mime-type.command.ts deleted file mode 100644 index 7f216ab03b..0000000000 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-18/1-18-backfill-file-size-and-mime-type.command.ts +++ /dev/null @@ -1,184 +0,0 @@ -import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; - -import { Command } from 'nest-commander'; -import { FileFolder } from 'twenty-shared/types'; -import { DataSource, Equal, LessThan, Repository } from 'typeorm'; - -import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner'; -import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner'; -import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service'; -import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity'; -import { extractFileInfo } from 'src/engine/core-modules/file/utils/extract-file-info.utils'; -import { removeFileFolderFromFileEntityPath } from 'src/engine/core-modules/file/utils/remove-file-folder-from-file-entity-path.utils'; -import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; -import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; -import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; -import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; -import { streamToBuffer } from 'src/utils/stream-to-buffer'; - -@Command({ - name: 'upgrade:1-18:backfill-file-size-and-mime-type', - description: - 'Backfill file size and mime type for files with missing or default values', -}) -export class BackfillFileSizeAndMimeTypeCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner { - constructor( - @InjectRepository(WorkspaceEntity) - protected readonly workspaceRepository: Repository, - protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager, - protected readonly dataSourceService: DataSourceService, - private readonly fileStorageService: FileStorageService, - private readonly workspaceCacheService: WorkspaceCacheService, - @InjectDataSource() - private readonly coreDataSource: DataSource, - ) { - super(workspaceRepository, twentyORMGlobalManager, dataSourceService); - } - - override async runOnWorkspace({ - workspaceId, - options, - }: RunOnWorkspaceArgs): Promise { - const isDryRun = options.dryRun ?? false; - - this.logger.log( - `${isDryRun ? '[DRY RUN] ' : ''}Starting file size and mime type backfill for workspace ${workspaceId}`, - ); - - const { flatApplicationMaps } = - await this.workspaceCacheService.getOrRecompute(workspaceId, [ - 'flatApplicationMaps', - ]); - - const fileRepository = this.coreDataSource.getRepository(FileEntity); - - const filesToBackfill = await fileRepository.find({ - where: [ - { - workspaceId, - size: LessThan(0), - }, - { - workspaceId, - mimeType: Equal('application/octet-stream'), - }, - ], - select: [ - 'id', - 'path', - 'applicationId', - 'workspaceId', - 'size', - 'mimeType', - ], - }); - - if (filesToBackfill.length === 0) { - this.logger.log(`No files to backfill for workspace ${workspaceId}`); - - return; - } - - this.logger.log( - `Found ${filesToBackfill.length} file(s) to backfill in workspace ${workspaceId}`, - ); - - for (const fileEntity of filesToBackfill) { - try { - this.logger.log( - `Processing file ${fileEntity.id} (path: ${fileEntity.path})`, - ); - - const application = flatApplicationMaps.byId[fileEntity.applicationId]; - - if (!application) { - this.logger.error( - `Application not found for file ${fileEntity.id} (applicationId: ${fileEntity.applicationId})`, - ); - - continue; - } - - const [fileFolder] = fileEntity.path.split('/') as [FileFolder]; - - if (!Object.values(FileFolder).includes(fileFolder)) { - this.logger.error( - `Invalid file folder '${fileFolder}' for file ${fileEntity.id} (path: ${fileEntity.path})`, - ); - - continue; - } - - const fileExists = await this.fileStorageService.checkFileExists({ - fileFolder, - applicationUniversalIdentifier: application.universalIdentifier, - workspaceId, - resourcePath: removeFileFolderFromFileEntityPath(fileEntity.path), - }); - - if (!fileExists) { - this.logger.error( - `File ${fileEntity.id} (path: ${fileEntity.path}) not found in storage`, - ); - - continue; - } - - const fileStream = await this.fileStorageService.readFile({ - fileFolder, - applicationUniversalIdentifier: application.universalIdentifier, - workspaceId, - resourcePath: removeFileFolderFromFileEntityPath(fileEntity.path), - }); - - const fileBuffer = await streamToBuffer(fileStream); - - const updateData: { - size?: number; - mimeType?: string; - } = {}; - - if (fileEntity.size < 0) { - updateData.size = fileBuffer.length; - } - - if (fileEntity.mimeType === 'application/octet-stream') { - try { - const { mimeType } = await extractFileInfo({ - file: fileBuffer, - filename: fileEntity.path, - }); - - updateData.mimeType = mimeType; - } catch (error) { - this.logger.error( - `Failed to extract file mime type for file ${fileEntity.id} in workspace ${workspaceId}: ${error.message}`, - ); - - continue; - } - } - - if (!isDryRun) { - await fileRepository.update({ id: fileEntity.id }, updateData); - } - - const updateDetails = Object.entries(updateData) - .map(([key, value]) => `${key}=${value}`) - .join(', '); - - this.logger.log( - `${isDryRun ? '[DRY RUN] ' : ''}Backfilled file ${fileEntity.id}: ${updateDetails}`, - ); - } catch (error) { - this.logger.error( - `Failed to backfill file ${fileEntity.id} in workspace ${workspaceId}: ${error.message}`, - ); - } - } - - this.logger.log( - `${isDryRun ? '[DRY RUN] ' : ''}Completed file size and mime type backfill for workspace ${workspaceId}`, - ); - } -} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-18/1-18-backfill-message-channel-throttle-retry-after.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-18/1-18-backfill-message-channel-throttle-retry-after.command.ts deleted file mode 100644 index 8c6553ddc1..0000000000 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-18/1-18-backfill-message-channel-throttle-retry-after.command.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { InjectRepository } from '@nestjs/typeorm'; - -import { Command } from 'nest-commander'; -import { STANDARD_OBJECTS } from 'twenty-shared/metadata'; -import { FieldMetadataType } from 'twenty-shared/types'; -import { Repository } from 'typeorm'; - -import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner'; -import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner'; -import { ApplicationService } from 'src/engine/core-modules/application/application.service'; -import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; -import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; -import { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata.service'; -import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.util'; -import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; -import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; -import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; - -@Command({ - name: 'upgrade:1-18:backfill-message-channel-throttle-retry-after', - description: - 'Backfill throttleRetryAfter field on messageChannel standard object', -}) -export class BackfillMessageChannelThrottleRetryAfterCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner { - constructor( - @InjectRepository(WorkspaceEntity) - protected readonly workspaceRepository: Repository, - protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager, - protected readonly dataSourceService: DataSourceService, - private readonly applicationService: ApplicationService, - private readonly fieldMetadataService: FieldMetadataService, - private readonly workspaceCacheService: WorkspaceCacheService, - ) { - super(workspaceRepository, twentyORMGlobalManager, dataSourceService); - } - - override async runOnWorkspace({ - workspaceId, - options, - }: RunOnWorkspaceArgs): Promise { - this.logger.log( - `Backfilling throttleRetryAfter field for workspace ${workspaceId}`, - ); - - const { flatObjectMetadataMaps } = - await this.workspaceCacheService.getOrRecompute(workspaceId, [ - 'flatObjectMetadataMaps', - ]); - - const messageChannelObjectMetadata = - findFlatEntityByUniversalIdentifier({ - flatEntityMaps: flatObjectMetadataMaps, - universalIdentifier: - STANDARD_OBJECTS.messageChannel.universalIdentifier, - }); - - if (!messageChannelObjectMetadata) { - this.logger.log( - `MessageChannel object metadata not found for workspace ${workspaceId}. Skipping.`, - ); - - return; - } - - const { flatFieldMetadataMaps } = - await this.workspaceCacheService.getOrRecompute(workspaceId, [ - 'flatFieldMetadataMaps', - ]); - - const existingField = findFlatEntityByUniversalIdentifier({ - flatEntityMaps: flatFieldMetadataMaps, - universalIdentifier: - STANDARD_OBJECTS.messageChannel.fields.throttleRetryAfter - .universalIdentifier, - }); - - if (existingField) { - this.logger.log( - `throttleRetryAfter field already exists for workspace ${workspaceId}. Skipping.`, - ); - - return; - } - - if (options.dryRun) { - this.logger.log( - `Would have created throttleRetryAfter field for workspace ${workspaceId}. Skipping (dry run).`, - ); - - return; - } - - const { twentyStandardFlatApplication } = - await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow( - { workspaceId }, - ); - - await this.fieldMetadataService.createOneField({ - createFieldInput: { - name: 'throttleRetryAfter', - type: FieldMetadataType.DATE_TIME, - label: 'Throttle Retry After', - description: 'Throttle Retry After', - icon: 'IconClock', - isNullable: true, - isUIReadOnly: true, - objectMetadataId: messageChannelObjectMetadata.id, - universalIdentifier: - STANDARD_OBJECTS.messageChannel.fields.throttleRetryAfter - .universalIdentifier, - }, - workspaceId, - ownerFlatApplication: twentyStandardFlatApplication, - }); - - this.logger.log( - `Successfully created throttleRetryAfter field for workspace ${workspaceId}`, - ); - } -} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-18/1-18-backfill-standard-views-and-field-metadata.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-18/1-18-backfill-standard-views-and-field-metadata.command.ts deleted file mode 100644 index 66f07c267b..0000000000 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-18/1-18-backfill-standard-views-and-field-metadata.command.ts +++ /dev/null @@ -1,753 +0,0 @@ -import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; - -import { Command } from 'nest-commander'; -import { STANDARD_OBJECTS } from 'twenty-shared/metadata'; -import { DataSource, Repository } from 'typeorm'; - -import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner'; -import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner'; -import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; -import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; -import { getMetadataFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-flat-entity-maps-key.util'; -import { getMetadataRelatedMetadataNames } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-related-metadata-names.util'; -import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service'; -import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; -import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; -import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; -import { type WorkspaceCacheKeyName } from 'src/engine/workspace-cache/types/workspace-cache-key.type'; -import { TWENTY_STANDARD_APPLICATION } from 'src/engine/workspace-manager/twenty-standard-application/constants/twenty-standard-applications'; - -// Fields that should be marked as isSystem: true -const FIELDS_TO_MARK_AS_SYSTEM = [ - // attachment: deprecated fields - STANDARD_OBJECTS.attachment.fields.fullPath.universalIdentifier, - STANDARD_OBJECTS.attachment.fields.fileCategory.universalIdentifier, - // timelineActivity: linked record fields - STANDARD_OBJECTS.timelineActivity.fields.linkedRecordCachedName - .universalIdentifier, - STANDARD_OBJECTS.timelineActivity.fields.linkedRecordId.universalIdentifier, - STANDARD_OBJECTS.timelineActivity.fields.linkedObjectMetadataId - .universalIdentifier, - // workspaceMember: relational fields - STANDARD_OBJECTS.workspaceMember.fields.favorites.universalIdentifier, - STANDARD_OBJECTS.workspaceMember.fields.accountOwnerForCompanies - .universalIdentifier, - STANDARD_OBJECTS.workspaceMember.fields.connectedAccounts.universalIdentifier, - STANDARD_OBJECTS.workspaceMember.fields.messageParticipants - .universalIdentifier, - STANDARD_OBJECTS.workspaceMember.fields.blocklist.universalIdentifier, - STANDARD_OBJECTS.workspaceMember.fields.calendarEventParticipants - .universalIdentifier, -]; - -// Morph target fields that should have label "Target" and icon "IconArrowUpRight" -const MORPH_FIELDS_TO_RELABEL = [ - // attachment - 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, - // noteTarget - STANDARD_OBJECTS.noteTarget.fields.targetPerson.universalIdentifier, - STANDARD_OBJECTS.noteTarget.fields.targetCompany.universalIdentifier, - STANDARD_OBJECTS.noteTarget.fields.targetOpportunity.universalIdentifier, - // taskTarget - STANDARD_OBJECTS.taskTarget.fields.targetPerson.universalIdentifier, - STANDARD_OBJECTS.taskTarget.fields.targetCompany.universalIdentifier, - STANDARD_OBJECTS.taskTarget.fields.targetOpportunity.universalIdentifier, - // timelineActivity - STANDARD_OBJECTS.timelineActivity.fields.targetPerson.universalIdentifier, - STANDARD_OBJECTS.timelineActivity.fields.targetCompany.universalIdentifier, - STANDARD_OBJECTS.timelineActivity.fields.targetOpportunity - .universalIdentifier, - STANDARD_OBJECTS.timelineActivity.fields.targetTask.universalIdentifier, - STANDARD_OBJECTS.timelineActivity.fields.targetNote.universalIdentifier, - STANDARD_OBJECTS.timelineActivity.fields.targetWorkflow.universalIdentifier, - STANDARD_OBJECTS.timelineActivity.fields.targetWorkflowVersion - .universalIdentifier, - STANDARD_OBJECTS.timelineActivity.fields.targetWorkflowRun - .universalIdentifier, - STANDARD_OBJECTS.timelineActivity.fields.targetDashboard.universalIdentifier, -]; - -interface ViewFieldDef { - fieldUI: string; - viewFieldUI: string; - position: number; -} - -interface ViewToBackfill { - objectUI: string; - viewName: string; - viewIcon: string; - viewUI: string; - viewFields: ViewFieldDef[]; -} - -const VIEWS_TO_BACKFILL: ViewToBackfill[] = [ - { - objectUI: STANDARD_OBJECTS.attachment.universalIdentifier, - viewName: 'All Attachments', - viewIcon: 'IconList', - viewUI: - STANDARD_OBJECTS.attachment.views.allAttachments.universalIdentifier, - viewFields: [ - { - fieldUI: STANDARD_OBJECTS.attachment.fields.name.universalIdentifier, - viewFieldUI: - STANDARD_OBJECTS.attachment.views.allAttachments.viewFields.name - .universalIdentifier, - position: 0, - }, - { - fieldUI: STANDARD_OBJECTS.attachment.fields.file.universalIdentifier, - viewFieldUI: - STANDARD_OBJECTS.attachment.views.allAttachments.viewFields.file - .universalIdentifier, - position: 1, - }, - { - fieldUI: - STANDARD_OBJECTS.attachment.fields.targetPerson.universalIdentifier, - viewFieldUI: - STANDARD_OBJECTS.attachment.views.allAttachments.viewFields - .targetPerson.universalIdentifier, - position: 2, - }, - { - fieldUI: - STANDARD_OBJECTS.attachment.fields.targetCompany.universalIdentifier, - viewFieldUI: - STANDARD_OBJECTS.attachment.views.allAttachments.viewFields - .targetCompany.universalIdentifier, - position: 3, - }, - { - fieldUI: - STANDARD_OBJECTS.attachment.fields.targetOpportunity - .universalIdentifier, - viewFieldUI: - STANDARD_OBJECTS.attachment.views.allAttachments.viewFields - .targetOpportunity.universalIdentifier, - position: 4, - }, - { - fieldUI: - STANDARD_OBJECTS.attachment.fields.targetTask.universalIdentifier, - viewFieldUI: - STANDARD_OBJECTS.attachment.views.allAttachments.viewFields.targetTask - .universalIdentifier, - position: 5, - }, - { - fieldUI: - STANDARD_OBJECTS.attachment.fields.targetNote.universalIdentifier, - viewFieldUI: - STANDARD_OBJECTS.attachment.views.allAttachments.viewFields.targetNote - .universalIdentifier, - position: 6, - }, - { - fieldUI: - STANDARD_OBJECTS.attachment.fields.targetDashboard - .universalIdentifier, - viewFieldUI: - STANDARD_OBJECTS.attachment.views.allAttachments.viewFields - .targetDashboard.universalIdentifier, - position: 7, - }, - { - fieldUI: - STANDARD_OBJECTS.attachment.fields.targetWorkflow.universalIdentifier, - viewFieldUI: - STANDARD_OBJECTS.attachment.views.allAttachments.viewFields - .targetWorkflow.universalIdentifier, - position: 8, - }, - { - fieldUI: - STANDARD_OBJECTS.attachment.fields.createdBy.universalIdentifier, - viewFieldUI: - STANDARD_OBJECTS.attachment.views.allAttachments.viewFields.createdBy - .universalIdentifier, - position: 9, - }, - { - fieldUI: - STANDARD_OBJECTS.attachment.fields.createdAt.universalIdentifier, - viewFieldUI: - STANDARD_OBJECTS.attachment.views.allAttachments.viewFields.createdAt - .universalIdentifier, - position: 10, - }, - ], - }, - { - objectUI: STANDARD_OBJECTS.noteTarget.universalIdentifier, - viewName: 'All Note Targets', - viewIcon: 'IconList', - viewUI: - STANDARD_OBJECTS.noteTarget.views.allNoteTargets.universalIdentifier, - viewFields: [ - { - fieldUI: STANDARD_OBJECTS.noteTarget.fields.id.universalIdentifier, - viewFieldUI: - STANDARD_OBJECTS.noteTarget.views.allNoteTargets.viewFields.id - .universalIdentifier, - position: 0, - }, - { - fieldUI: STANDARD_OBJECTS.noteTarget.fields.note.universalIdentifier, - viewFieldUI: - STANDARD_OBJECTS.noteTarget.views.allNoteTargets.viewFields.note - .universalIdentifier, - position: 1, - }, - { - fieldUI: - STANDARD_OBJECTS.noteTarget.fields.targetPerson.universalIdentifier, - viewFieldUI: - STANDARD_OBJECTS.noteTarget.views.allNoteTargets.viewFields - .targetPerson.universalIdentifier, - position: 2, - }, - { - fieldUI: - STANDARD_OBJECTS.noteTarget.fields.targetCompany.universalIdentifier, - viewFieldUI: - STANDARD_OBJECTS.noteTarget.views.allNoteTargets.viewFields - .targetCompany.universalIdentifier, - position: 3, - }, - { - fieldUI: - STANDARD_OBJECTS.noteTarget.fields.targetOpportunity - .universalIdentifier, - viewFieldUI: - STANDARD_OBJECTS.noteTarget.views.allNoteTargets.viewFields - .targetOpportunity.universalIdentifier, - position: 4, - }, - ], - }, - { - objectUI: STANDARD_OBJECTS.taskTarget.universalIdentifier, - viewName: 'All Task Targets', - viewIcon: 'IconList', - viewUI: - STANDARD_OBJECTS.taskTarget.views.allTaskTargets.universalIdentifier, - viewFields: [ - { - fieldUI: STANDARD_OBJECTS.taskTarget.fields.id.universalIdentifier, - viewFieldUI: - STANDARD_OBJECTS.taskTarget.views.allTaskTargets.viewFields.id - .universalIdentifier, - position: 0, - }, - { - fieldUI: STANDARD_OBJECTS.taskTarget.fields.task.universalIdentifier, - viewFieldUI: - STANDARD_OBJECTS.taskTarget.views.allTaskTargets.viewFields.task - .universalIdentifier, - position: 1, - }, - { - fieldUI: - STANDARD_OBJECTS.taskTarget.fields.targetPerson.universalIdentifier, - viewFieldUI: - STANDARD_OBJECTS.taskTarget.views.allTaskTargets.viewFields - .targetPerson.universalIdentifier, - position: 2, - }, - { - fieldUI: - STANDARD_OBJECTS.taskTarget.fields.targetCompany.universalIdentifier, - viewFieldUI: - STANDARD_OBJECTS.taskTarget.views.allTaskTargets.viewFields - .targetCompany.universalIdentifier, - position: 3, - }, - { - fieldUI: - STANDARD_OBJECTS.taskTarget.fields.targetOpportunity - .universalIdentifier, - viewFieldUI: - STANDARD_OBJECTS.taskTarget.views.allTaskTargets.viewFields - .targetOpportunity.universalIdentifier, - position: 4, - }, - ], - }, - { - objectUI: STANDARD_OBJECTS.timelineActivity.universalIdentifier, - viewName: 'All Timeline Activities', - viewIcon: 'IconList', - viewUI: - STANDARD_OBJECTS.timelineActivity.views.allTimelineActivities - .universalIdentifier, - viewFields: [ - { - fieldUI: - STANDARD_OBJECTS.timelineActivity.fields.name.universalIdentifier, - viewFieldUI: - STANDARD_OBJECTS.timelineActivity.views.allTimelineActivities - .viewFields.name.universalIdentifier, - position: 0, - }, - { - fieldUI: - STANDARD_OBJECTS.timelineActivity.fields.happensAt - .universalIdentifier, - viewFieldUI: - STANDARD_OBJECTS.timelineActivity.views.allTimelineActivities - .viewFields.happensAt.universalIdentifier, - position: 1, - }, - { - fieldUI: - STANDARD_OBJECTS.timelineActivity.fields.targetPerson - .universalIdentifier, - viewFieldUI: - STANDARD_OBJECTS.timelineActivity.views.allTimelineActivities - .viewFields.targetPerson.universalIdentifier, - position: 2, - }, - { - fieldUI: - STANDARD_OBJECTS.timelineActivity.fields.targetCompany - .universalIdentifier, - viewFieldUI: - STANDARD_OBJECTS.timelineActivity.views.allTimelineActivities - .viewFields.targetCompany.universalIdentifier, - position: 3, - }, - { - fieldUI: - STANDARD_OBJECTS.timelineActivity.fields.targetOpportunity - .universalIdentifier, - viewFieldUI: - STANDARD_OBJECTS.timelineActivity.views.allTimelineActivities - .viewFields.targetOpportunity.universalIdentifier, - position: 4, - }, - { - fieldUI: - STANDARD_OBJECTS.timelineActivity.fields.targetTask - .universalIdentifier, - viewFieldUI: - STANDARD_OBJECTS.timelineActivity.views.allTimelineActivities - .viewFields.targetTask.universalIdentifier, - position: 5, - }, - { - fieldUI: - STANDARD_OBJECTS.timelineActivity.fields.targetNote - .universalIdentifier, - viewFieldUI: - STANDARD_OBJECTS.timelineActivity.views.allTimelineActivities - .viewFields.targetNote.universalIdentifier, - position: 6, - }, - { - fieldUI: - STANDARD_OBJECTS.timelineActivity.fields.targetWorkflow - .universalIdentifier, - viewFieldUI: - STANDARD_OBJECTS.timelineActivity.views.allTimelineActivities - .viewFields.targetWorkflow.universalIdentifier, - position: 7, - }, - { - fieldUI: - STANDARD_OBJECTS.timelineActivity.fields.targetWorkflowVersion - .universalIdentifier, - viewFieldUI: - STANDARD_OBJECTS.timelineActivity.views.allTimelineActivities - .viewFields.targetWorkflowVersion.universalIdentifier, - position: 8, - }, - { - fieldUI: - STANDARD_OBJECTS.timelineActivity.fields.targetWorkflowRun - .universalIdentifier, - viewFieldUI: - STANDARD_OBJECTS.timelineActivity.views.allTimelineActivities - .viewFields.targetWorkflowRun.universalIdentifier, - position: 9, - }, - { - fieldUI: - STANDARD_OBJECTS.timelineActivity.fields.targetDashboard - .universalIdentifier, - viewFieldUI: - STANDARD_OBJECTS.timelineActivity.views.allTimelineActivities - .viewFields.targetDashboard.universalIdentifier, - position: 10, - }, - { - fieldUI: - STANDARD_OBJECTS.timelineActivity.fields.workspaceMember - .universalIdentifier, - viewFieldUI: - STANDARD_OBJECTS.timelineActivity.views.allTimelineActivities - .viewFields.workspaceMember.universalIdentifier, - position: 11, - }, - { - fieldUI: - STANDARD_OBJECTS.timelineActivity.fields.properties - .universalIdentifier, - viewFieldUI: - STANDARD_OBJECTS.timelineActivity.views.allTimelineActivities - .viewFields.properties.universalIdentifier, - position: 12, - }, - { - fieldUI: - STANDARD_OBJECTS.timelineActivity.fields.linkedRecordCachedName - .universalIdentifier, - viewFieldUI: - STANDARD_OBJECTS.timelineActivity.views.allTimelineActivities - .viewFields.linkedRecordCachedName.universalIdentifier, - position: 13, - }, - ], - }, - { - objectUI: STANDARD_OBJECTS.workspaceMember.universalIdentifier, - viewName: 'All Workspace Members', - viewIcon: 'IconList', - viewUI: - STANDARD_OBJECTS.workspaceMember.views.allWorkspaceMembers - .universalIdentifier, - viewFields: [ - { - fieldUI: - STANDARD_OBJECTS.workspaceMember.fields.name.universalIdentifier, - viewFieldUI: - STANDARD_OBJECTS.workspaceMember.views.allWorkspaceMembers.viewFields - .name.universalIdentifier, - position: 0, - }, - { - fieldUI: - STANDARD_OBJECTS.workspaceMember.fields.userEmail.universalIdentifier, - viewFieldUI: - STANDARD_OBJECTS.workspaceMember.views.allWorkspaceMembers.viewFields - .userEmail.universalIdentifier, - position: 1, - }, - { - fieldUI: - STANDARD_OBJECTS.workspaceMember.fields.avatarUrl.universalIdentifier, - viewFieldUI: - STANDARD_OBJECTS.workspaceMember.views.allWorkspaceMembers.viewFields - .avatarUrl.universalIdentifier, - position: 2, - }, - { - fieldUI: - STANDARD_OBJECTS.workspaceMember.fields.colorScheme - .universalIdentifier, - viewFieldUI: - STANDARD_OBJECTS.workspaceMember.views.allWorkspaceMembers.viewFields - .colorScheme.universalIdentifier, - position: 3, - }, - { - fieldUI: - STANDARD_OBJECTS.workspaceMember.fields.locale.universalIdentifier, - viewFieldUI: - STANDARD_OBJECTS.workspaceMember.views.allWorkspaceMembers.viewFields - .locale.universalIdentifier, - position: 4, - }, - { - fieldUI: - STANDARD_OBJECTS.workspaceMember.fields.timeZone.universalIdentifier, - viewFieldUI: - STANDARD_OBJECTS.workspaceMember.views.allWorkspaceMembers.viewFields - .timeZone.universalIdentifier, - position: 5, - }, - { - fieldUI: - STANDARD_OBJECTS.workspaceMember.fields.dateFormat - .universalIdentifier, - viewFieldUI: - STANDARD_OBJECTS.workspaceMember.views.allWorkspaceMembers.viewFields - .dateFormat.universalIdentifier, - position: 6, - }, - { - fieldUI: - STANDARD_OBJECTS.workspaceMember.fields.timeFormat - .universalIdentifier, - viewFieldUI: - STANDARD_OBJECTS.workspaceMember.views.allWorkspaceMembers.viewFields - .timeFormat.universalIdentifier, - position: 7, - }, - { - fieldUI: - STANDARD_OBJECTS.workspaceMember.fields.createdAt.universalIdentifier, - viewFieldUI: - STANDARD_OBJECTS.workspaceMember.views.allWorkspaceMembers.viewFields - .createdAt.universalIdentifier, - position: 8, - }, - { - fieldUI: - STANDARD_OBJECTS.workspaceMember.fields.ownedOpportunities - .universalIdentifier, - viewFieldUI: - STANDARD_OBJECTS.workspaceMember.views.allWorkspaceMembers.viewFields - .ownedOpportunities.universalIdentifier, - position: 9, - }, - { - fieldUI: - STANDARD_OBJECTS.workspaceMember.fields.assignedTasks - .universalIdentifier, - viewFieldUI: - STANDARD_OBJECTS.workspaceMember.views.allWorkspaceMembers.viewFields - .assignedTasks.universalIdentifier, - position: 10, - }, - ], - }, -]; - -@Command({ - name: 'upgrade:1-18:backfill-standard-views-and-field-metadata', - description: - 'Backfill standard views and fix field metadata (isSystem, labels, icons) for attachment, noteTarget, taskTarget, timelineActivity, workspaceMember', -}) -export class BackfillStandardViewsAndFieldMetadataCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner { - constructor( - @InjectRepository(WorkspaceEntity) - protected readonly workspaceRepository: Repository, - @InjectDataSource() - private readonly coreDataSource: DataSource, - protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager, - protected readonly dataSourceService: DataSourceService, - private readonly workspaceCacheService: WorkspaceCacheService, - private readonly workspaceCacheStorageService: WorkspaceCacheStorageService, - private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService, - ) { - super(workspaceRepository, twentyORMGlobalManager, dataSourceService); - } - - override async runOnWorkspace({ - workspaceId, - options, - }: RunOnWorkspaceArgs): Promise { - const dryRun = options?.dryRun ?? false; - - this.logger.log( - `${dryRun ? '[DRY RUN] ' : ''}Backfilling views and field metadata for workspace ${workspaceId}`, - ); - - if (dryRun) { - this.logger.log( - `[DRY RUN] Would update isSystem, labels/icons, backfill views and viewFields for workspace ${workspaceId}. Skipping.`, - ); - - return; - } - - const queryRunner = this.coreDataSource.createQueryRunner(); - - await queryRunner.connect(); - await queryRunner.startTransaction(); - - try { - // Part 1: Mark fields as isSystem - const systemResult = await queryRunner.query( - `UPDATE core."fieldMetadata" - SET "isSystem" = true - WHERE "workspaceId" = $1 - AND "universalIdentifier" = ANY($2) - AND "isSystem" = false`, - [workspaceId, FIELDS_TO_MARK_AS_SYSTEM], - ); - - const systemCount = systemResult?.[1] ?? 0; - - if (systemCount > 0) { - this.logger.log( - `Marked ${systemCount} field(s) as isSystem for workspace ${workspaceId}`, - ); - } - - // Part 1b: Update morph field labels and icons to "Target" - const labelResult = await queryRunner.query( - `UPDATE core."fieldMetadata" - SET label = 'Target', icon = 'IconArrowUpRight' - WHERE "workspaceId" = $1 - AND "universalIdentifier" = ANY($2) - AND (label != 'Target' OR icon != 'IconArrowUpRight')`, - [workspaceId, MORPH_FIELDS_TO_RELABEL], - ); - - const labelCount = labelResult?.[1] ?? 0; - - if (labelCount > 0) { - this.logger.log( - `Relabeled ${labelCount} morph field(s) to "Target" for workspace ${workspaceId}`, - ); - } - - // Part 2: Resolve applicationId for twenty-standard app - const applicationRows = await queryRunner.query( - `SELECT id FROM core."application" - WHERE "workspaceId" = $1 - AND "universalIdentifier" = $2`, - [workspaceId, TWENTY_STANDARD_APPLICATION.universalIdentifier], - ); - - if (applicationRows.length === 0) { - this.logger.warn( - `Twenty Standard Application not found for workspace ${workspaceId}. Skipping view backfill.`, - ); - await queryRunner.commitTransaction(); - await this.invalidateCaches(workspaceId); - - return; - } - - const applicationId = applicationRows[0].id; - - // Part 3 & 4: Backfill views and viewFields - for (const view of VIEWS_TO_BACKFILL) { - // Insert view if it doesn't exist - const viewInsertResult = await queryRunner.query( - `INSERT INTO core."view" ( - id, name, "objectMetadataId", type, icon, position, - visibility, "workspaceId", "applicationId", "universalIdentifier" - ) - SELECT - gen_random_uuid(), $2, om.id, 'TABLE', $3, 0, - 'WORKSPACE', $1, $4, $5 - FROM core."objectMetadata" om - WHERE om."workspaceId" = $1 - AND om."universalIdentifier" = $6 - AND NOT EXISTS ( - SELECT 1 FROM core."view" v - WHERE v."workspaceId" = $1 - AND v."universalIdentifier" = $5 - AND v."deletedAt" IS NULL - ) - RETURNING 1`, - [ - workspaceId, - view.viewName, - view.viewIcon, - applicationId, - view.viewUI, - view.objectUI, - ], - ); - - const viewInserted = viewInsertResult?.length ?? 0; - - if (viewInserted > 0) { - this.logger.log( - `Created view "${view.viewName}" for workspace ${workspaceId}`, - ); - } - - // Insert viewFields - for (const viewField of view.viewFields) { - await queryRunner.query( - `INSERT INTO core."viewField" ( - id, "fieldMetadataId", "isVisible", size, position, - "viewId", "workspaceId", "applicationId", "universalIdentifier" - ) - SELECT - gen_random_uuid(), fm.id, true, 150, $4, - v.id, $1, $5, $6 - FROM core."view" v - JOIN core."fieldMetadata" fm - ON fm."workspaceId" = $1 - AND fm."universalIdentifier" = $3 - WHERE v."workspaceId" = $1 - AND v."universalIdentifier" = $2 - AND v."deletedAt" IS NULL - AND NOT EXISTS ( - SELECT 1 FROM core."viewField" vf - WHERE vf."workspaceId" = $1 - AND vf."universalIdentifier" = $6 - AND vf."deletedAt" IS NULL - )`, - [ - workspaceId, - view.viewUI, - viewField.fieldUI, - viewField.position, - applicationId, - viewField.viewFieldUI, - ], - ); - } - } - - await queryRunner.commitTransaction(); - - this.logger.log(`Completed for workspace ${workspaceId}`); - - await this.invalidateCaches(workspaceId); - } catch (error) { - if (queryRunner.isTransactionActive) { - await queryRunner.rollbackTransaction(); - this.logger.error( - `Error backfilling views and field metadata (rolled back) for workspace ${workspaceId}`, - error, - ); - } else { - this.logger.error( - `Error backfilling views and field metadata (after commit) for workspace ${workspaceId}`, - error, - ); - } - throw error; - } finally { - await queryRunner.release(); - } - } - - private async invalidateCaches(workspaceId: string): Promise { - const modifiedMetadataNames = [ - 'fieldMetadata', - 'view', - 'viewField', - ] as const; - - const cacheKeysToInvalidate: WorkspaceCacheKeyName[] = [ - ...new Set( - modifiedMetadataNames - .flatMap((name) => [name, ...getMetadataRelatedMetadataNames(name)]) - .map(getMetadataFlatEntityMapsKey), - ), - 'ORMEntityMetadatas', - ]; - - await this.workspaceCacheService.invalidateAndRecompute( - workspaceId, - cacheKeysToInvalidate, - ); - - await this.workspaceMetadataVersionService.incrementMetadataVersion( - workspaceId, - ); - - await this.workspaceCacheStorageService.flush(workspaceId); - - this.logger.log( - `Cache invalidated and metadata version incremented for workspace ${workspaceId}`, - ); - } -} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-18/1-18-delete-orphan-favorites.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-18/1-18-delete-orphan-favorites.command.ts deleted file mode 100644 index 37557b6773..0000000000 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-18/1-18-delete-orphan-favorites.command.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { Logger } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; - -import { Command } from 'nest-commander'; -import { isDefined } from 'twenty-shared/utils'; -import { IsNull, Not, Repository } from 'typeorm'; - -import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner'; -import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner'; -import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; -import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; -import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; -import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; -import { FavoriteWorkspaceEntity } from 'src/modules/favorite/standard-objects/favorite.workspace-entity'; - -@Command({ - name: 'upgrade:1-18:delete-orphan-favorites', - description: - 'Delete favorites whose viewId does not exist in workspace view metadata (fixes upgrade failure)', -}) -export class DeleteOrphanFavoritesCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner { - protected readonly logger = new Logger(DeleteOrphanFavoritesCommand.name); - - constructor( - @InjectRepository(WorkspaceEntity) - protected readonly workspaceRepository: Repository, - protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager, - protected readonly dataSourceService: DataSourceService, - private readonly workspaceCacheService: WorkspaceCacheService, - ) { - super(workspaceRepository, twentyORMGlobalManager, dataSourceService); - } - - override async runOnWorkspace({ - workspaceId, - options, - }: RunOnWorkspaceArgs): Promise { - this.logger.log(`Deleting orphan favorites for workspace ${workspaceId}`); - - const { flatViewMaps } = await this.workspaceCacheService.getOrRecompute( - workspaceId, - ['flatViewMaps'], - ); - - const favoriteRepository = - await this.twentyORMGlobalManager.getRepository( - workspaceId, - 'favorite', - { shouldBypassPermissionChecks: true }, - ); - - const favoritesWithViewId = await favoriteRepository.find({ - where: { - deletedAt: IsNull(), - viewId: Not(IsNull()), - }, - select: { id: true, viewId: true }, - }); - - const orphanFavoriteIds = favoritesWithViewId - .filter( - (favorite) => - !isDefined(flatViewMaps.universalIdentifierById[favorite.viewId]), - ) - .map((favorite) => favorite.id); - - if (orphanFavoriteIds.length === 0) { - this.logger.log(`No orphan favorites found for workspace ${workspaceId}`); - - return; - } - - if (options.dryRun) { - this.logger.log( - `[DRY RUN] Would delete ${orphanFavoriteIds.length} orphan favorite(s) for workspace ${workspaceId}`, - ); - - return; - } - - await favoriteRepository.delete(orphanFavoriteIds); - - this.logger.log( - `Deleted ${orphanFavoriteIds.length} orphan favorite(s) for workspace ${workspaceId}`, - ); - } -} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-18/1-18-migrate-activity-rich-text-attachment-file-ids.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-18/1-18-migrate-activity-rich-text-attachment-file-ids.command.ts deleted file mode 100644 index 96f179dc4b..0000000000 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-18/1-18-migrate-activity-rich-text-attachment-file-ids.command.ts +++ /dev/null @@ -1,468 +0,0 @@ -import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; - -import { isNonEmptyString } from '@sniptt/guards'; -import { Command } from 'nest-commander'; -import { STANDARD_OBJECTS } from 'twenty-shared/metadata'; -import { FileFolder } from 'twenty-shared/types'; -import { - extractFolderPathFilenameAndTypeOrThrow, - isDefined, -} from 'twenty-shared/utils'; -import { DataSource, Repository } from 'typeorm'; -import { v4 } from 'uuid'; - -import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner'; -import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner'; -import { ApplicationService } from 'src/engine/core-modules/application/application.service'; -import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service'; -import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity'; -import { FileUrlService } from 'src/engine/core-modules/file/file-url/file-url.service'; -import { extractFileIdFromUrl } from 'src/engine/core-modules/file/files-field/utils/extract-file-id-from-url.util'; -import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; -import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; -import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.util'; -import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; -import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; -import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; -import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; -import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; -import { AttachmentWorkspaceEntity } from 'src/modules/attachment/standard-objects/attachment.workspace-entity'; -import { NoteWorkspaceEntity } from 'src/modules/note/standard-objects/note.workspace-entity'; -import { TaskWorkspaceEntity } from 'src/modules/task/standard-objects/task.workspace-entity'; - -type RichTextBlock = Record; - -@Command({ - name: 'upgrade:1-18:migrate-activity-rich-text-attachment-file-ids', - description: - '[DEPRECATED] Migrate activity rich text blocks - this migration is now complete and no longer needed', -}) -export class MigrateActivityRichTextAttachmentFileIdsCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner { - constructor( - @InjectRepository(WorkspaceEntity) - protected readonly workspaceRepository: Repository, - protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager, - protected readonly dataSourceService: DataSourceService, - private readonly fileStorageService: FileStorageService, - private readonly workspaceCacheService: WorkspaceCacheService, - private readonly applicationService: ApplicationService, - private readonly fileUrlService: FileUrlService, - @InjectDataSource() - private readonly coreDataSource: DataSource, - ) { - super(workspaceRepository, twentyORMGlobalManager, dataSourceService); - } - - override async runOnWorkspace({ - workspaceId, - }: RunOnWorkspaceArgs): Promise { - this.logger.log( - `[DEPRECATED] Activity rich text attachment file IDs migration is no longer needed for workspace ${workspaceId}. ` + - `The IS_FILES_FIELD_MIGRATED feature flag has been removed as all workspaces are now migrated.`, - ); - } - - private _deprecatedMigrationLogic = async ({ - workspaceId, - isDryRun, - }: { - workspaceId: string; - isDryRun: boolean; - }) => { - this.logger.log( - `${isDryRun ? '[DRY RUN] ' : ''}Starting activity rich text attachment file IDs migration for workspace ${workspaceId}`, - ); - - const { flatObjectMetadataMaps, flatFieldMetadataMaps } = - await this.workspaceCacheService.getOrRecompute(workspaceId, [ - 'flatObjectMetadataMaps', - 'flatFieldMetadataMaps', - ]); - - const attachmentObjectMetadata = - findFlatEntityByUniversalIdentifier({ - flatEntityMaps: flatObjectMetadataMaps, - universalIdentifier: STANDARD_OBJECTS.attachment.universalIdentifier, - }); - - if (!isDefined(attachmentObjectMetadata)) { - this.logger.warn( - `Attachment object metadata not found for workspace ${workspaceId}, skipping`, - ); - - return; - } - - const { twentyStandardFlatApplication } = - await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow( - { - workspaceId, - }, - ); - - const attachmentFileFieldMetadata = findFlatEntityByUniversalIdentifier({ - flatEntityMaps: flatFieldMetadataMaps, - universalIdentifier: - STANDARD_OBJECTS.attachment.fields.file.universalIdentifier, - }); - - if (!isDefined(attachmentFileFieldMetadata)) { - this.logger.error( - `Failed to find attachment file field metadata for workspace ${workspaceId}`, - ); - - throw new Error( - `Failed to find attachment file field metadata for workspace ${workspaceId}`, - ); - } - - const systemAuthContext = buildSystemAuthContext(workspaceId); - - await this.twentyORMGlobalManager.executeInWorkspaceContext(async () => { - const attachmentRepository = - await this.twentyORMGlobalManager.getRepository( - workspaceId, - 'attachment', - { shouldBypassPermissionChecks: true }, - ); - - const noteRepository = - await this.twentyORMGlobalManager.getRepository( - workspaceId, - 'note', - { shouldBypassPermissionChecks: true }, - ); - - const taskRepository = - await this.twentyORMGlobalManager.getRepository( - workspaceId, - 'task', - { shouldBypassPermissionChecks: true }, - ); - - const fileRepository = this.coreDataSource.getRepository(FileEntity); - - await this.migrateActivityTable({ - activityRepository: noteRepository, - attachmentRepository, - fileRepository, - activityType: 'note', - targetColumnName: 'targetNoteId', - workspaceId, - twentyStandardApplication: twentyStandardFlatApplication, - fileFieldMetadata: attachmentFileFieldMetadata, - isDryRun, - }); - - await this.migrateActivityTable({ - activityRepository: taskRepository, - attachmentRepository, - fileRepository, - activityType: 'task', - targetColumnName: 'targetTaskId', - workspaceId, - twentyStandardApplication: twentyStandardFlatApplication, - fileFieldMetadata: attachmentFileFieldMetadata, - isDryRun, - }); - - this.logger.log( - `${isDryRun ? '[DRY RUN] ' : ''}Completed activity rich text attachment file IDs migration for workspace ${workspaceId}`, - ); - }, systemAuthContext); - }; - - private async migrateActivityTable({ - activityRepository, - attachmentRepository, - fileRepository, - activityType, - targetColumnName, - workspaceId, - twentyStandardApplication, - fileFieldMetadata, - isDryRun, - }: { - activityRepository: - | WorkspaceRepository - | WorkspaceRepository; - attachmentRepository: Awaited< - ReturnType - >; - fileRepository: Repository; - activityType: 'note' | 'task'; - targetColumnName: 'targetNoteId' | 'targetTaskId'; - workspaceId: string; - twentyStandardApplication: { id: string; universalIdentifier: string }; - fileFieldMetadata: { universalIdentifier: string }; - isDryRun: boolean; - }): Promise { - this.logger.log( - `Migrating ${activityType} rich text for workspace ${workspaceId}`, - ); - - const activities = await activityRepository.find(); - - this.logger.log( - `Found ${activities.length} ${activityType}(s) to process in workspace ${workspaceId}`, - ); - - for (const activity of activities) { - const bodyV2 = activity.bodyV2; - - if (!isDefined(bodyV2?.blocknote)) { - continue; - } - - let blocknote: RichTextBlock[]; - - try { - blocknote = JSON.parse(bodyV2.blocknote); - } catch (error) { - this.logger.warn( - `Failed to parse bodyV2.blocknote for ${activityType} ${activity.id}: ${error.message}`, - ); - continue; - } - - const needsMigration = blocknote.some((block: RichTextBlock) => { - const props = (block.props as Record) || {}; - const url = props.url as string | undefined; - - return ( - isDefined(url) && - !isDefined(extractFileIdFromUrl(url, FileFolder.FilesField)) - ); - }); - - if (!needsMigration) { - continue; - } - - const urlToFileIdMap = await this.buildUrlToFileIdMap( - attachmentRepository, - targetColumnName, - activity.id, - ); - - let hasChanges = false; - const enrichedBlocknote = []; - - for (const block of blocknote) { - const props = (block.props as Record) || {}; - const url = props.url as string | undefined; - - if ( - !isDefined(url) || - isDefined(extractFileIdFromUrl(url, FileFolder.FilesField)) || - !url.includes('/files/attachment/') - ) { - enrichedBlocknote.push(block); - continue; - } - - let fileId = this.findMatchingFileId(url, urlToFileIdMap); - - if (!isDefined(fileId) && !isDryRun) { - const createdFileId = await this.createAttachmentFromUrl( - url, - block, - attachmentRepository, - fileRepository, - targetColumnName, - activity.id, - workspaceId, - twentyStandardApplication, - fileFieldMetadata, - ); - - if (isDefined(createdFileId)) { - fileId = createdFileId; - } - } - - if (isDefined(fileId)) { - this.logger.log( - `Enriching ${block.type} block in ${activityType} ${activity.id} with fileId: ${fileId}`, - ); - hasChanges = true; - - const signedUrl = this.fileUrlService.signFileByIdUrl({ - fileId, - workspaceId, - fileFolder: FileFolder.FilesField, - }); - - enrichedBlocknote.push({ - ...block, - props: { - ...props, - url: signedUrl, - }, - }); - } else { - enrichedBlocknote.push(block); - } - } - - if (hasChanges && !isDryRun) { - const updatedBodyV2 = { - ...bodyV2, - blocknote: JSON.stringify(enrichedBlocknote), - }; - - await activityRepository.update( - { id: activity.id }, - { bodyV2: updatedBodyV2 }, - ); - } - } - } - - private async buildUrlToFileIdMap( - attachmentRepository: Awaited< - ReturnType - >, - targetColumnName: 'targetNoteId' | 'targetTaskId', - activityId: string, - ): Promise> { - const urlToFileIdMap = new Map(); - - const attachments = await attachmentRepository.find({ - where: { - [targetColumnName]: activityId, - }, - select: ['fullPath', 'file'], - }); - - for (const attachment of attachments) { - if ( - isDefined(attachment.file) && - Array.isArray(attachment.file) && - attachment.file.length > 0 - ) { - const fileData = attachment.file[0]; - - if (isDefined(attachment.fullPath) && isDefined(fileData.fileId)) { - urlToFileIdMap.set(attachment.fullPath, fileData.fileId); - } - } - } - - return urlToFileIdMap; - } - - private findMatchingFileId( - url: string, - urlToFileIdMap: Map, - ): string | undefined { - const normalizedUrl = url.includes(FileFolder.FilesField) - ? this.getAttachmentNewdRelativePath(url) - : this.getAttachmentLegacyRelativePath(url); - - return urlToFileIdMap.get(normalizedUrl); - } - - private async createAttachmentFromUrl( - url: string, - block: RichTextBlock, - attachmentRepository: Awaited< - ReturnType - >, - fileRepository: Repository, - targetColumnName: 'targetNoteId' | 'targetTaskId', - activityId: string, - workspaceId: string, - twentyStandardApplication: { id: string; universalIdentifier: string }, - fileFieldMetadata: { universalIdentifier: string }, - ): Promise { - try { - const props = block.props as Record; - const blockName = (props.name as string) || ''; - - const filePath = this.getAttachmentLegacyRelativePath(url); - - const { - type: fileExtension, - filename, - folderPath, - } = extractFolderPathFilenameAndTypeOrThrow(filePath); - - const fileId = v4(); - const newFilename = `${fileId}${isNonEmptyString(fileExtension) ? `.${fileExtension}` : ''}`; - const newResourcePath = `${FileFolder.FilesField}/${fileFieldMetadata.universalIdentifier}/${newFilename}`; - - await this.fileStorageService.copyLegacy({ - from: { - folderPath: `workspace-${workspaceId}/${folderPath}`, - filename: filename, - }, - to: { - folderPath: `${workspaceId}/${twentyStandardApplication.universalIdentifier}`, - filename: newResourcePath, - }, - }); - - const fileEntity = fileRepository.create({ - id: fileId, - path: newResourcePath, - workspaceId, - applicationId: twentyStandardApplication.id, - size: -1, - settings: { - isTemporaryFile: true, - toDelete: false, - }, - }); - - await fileRepository.save(fileEntity); - - const attachmentName = isNonEmptyString(blockName) ? blockName : filename; - - await attachmentRepository.save({ - [targetColumnName]: activityId, - name: attachmentName, - file: [ - { - fileId: fileEntity.id, - label: attachmentName, - }, - ], - }); - - this.logger.log( - `Created attachment for ${block.type} block with URL: ${url}`, - ); - - return fileId; - } catch (error) { - this.logger.warn( - `Failed to create attachment from URL ${url}: ${error.message}`, - ); - - return undefined; - } - } - - getAttachmentLegacyRelativePath = (url: string): string => { - if (!url.includes('/files/attachment/')) { - throw new Error(`Invalid attachment legacy relative path: ${url}`); - } - - //https://example.com/files/attachment/token/filename.jpg -> attachment/filename.jpg - const attachmentPath = url.split('/files/attachment/')[1].split('/')[1]; - - return `${FileFolder.Attachment}/${attachmentPath}`; - }; - - getAttachmentNewdRelativePath = (url: string): string => { - if (!url.includes('/files-field/')) { - throw new Error(`Invalid new attachment path: ${url}`); - } - - //https://example.com/files-field/123/456/789/filename.jpg -> files-field/123/456/789/filename.jpg - const attachmentPath = url.split('/files-field/')[1]; - - return `${FileFolder.FilesField}/${attachmentPath}`; - }; -} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-18/1-18-migrate-attachment-files.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-18/1-18-migrate-attachment-files.command.ts deleted file mode 100644 index c1f27609f7..0000000000 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-18/1-18-migrate-attachment-files.command.ts +++ /dev/null @@ -1,297 +0,0 @@ -import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; - -import { isNonEmptyString } from '@sniptt/guards'; -import { Command } from 'nest-commander'; -import { STANDARD_OBJECTS } from 'twenty-shared/metadata'; -import { FieldMetadataType, FileFolder } from 'twenty-shared/types'; -import { - extractFolderPathFilenameAndTypeOrThrow, - isDefined, -} from 'twenty-shared/utils'; -import { DataSource, Equal, IsNull, Not, Or, Repository } from 'typeorm'; -import { v4 } from 'uuid'; - -import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner'; -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 { ApplicationService } from 'src/engine/core-modules/application/application.service'; -import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service'; -import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity'; -import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; -import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; -import { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata.service'; -import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.util'; -import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; -import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; -import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; -import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; -import { AttachmentWorkspaceEntity } from 'src/modules/attachment/standard-objects/attachment.workspace-entity'; - -@Command({ - name: 'upgrade:1-18:migrate-attachment-files', - description: - '[DEPRECATED] Migrate attachment files to file field - this migration is now complete and no longer needed', -}) -export class MigrateAttachmentFilesCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner { - constructor( - @InjectRepository(WorkspaceEntity) - protected readonly workspaceRepository: Repository, - protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager, - protected readonly dataSourceService: DataSourceService, - private readonly fileStorageService: FileStorageService, - private readonly workspaceCacheService: WorkspaceCacheService, - private readonly fieldMetadataService: FieldMetadataService, - private readonly applicationService: ApplicationService, - @InjectDataSource() - private readonly coreDataSource: DataSource, - ) { - super(workspaceRepository, twentyORMGlobalManager, dataSourceService); - } - - override async runOnWorkspace({ - workspaceId, - }: RunOnWorkspaceArgs): Promise { - this.logger.log( - `[DEPRECATED] Attachment files migration is no longer needed for workspace ${workspaceId}. ` + - `The IS_FILES_FIELD_MIGRATED feature flag has been removed as all workspaces are now migrated.`, - ); - } - - private _deprecatedMigrationLogic = async ({ - workspaceId, - isDryRun, - }: { - workspaceId: string; - isDryRun: boolean; - }) => { - this.logger.log( - `${isDryRun ? '[DRY RUN] ' : ''}Starting attachment files migration for workspace ${workspaceId}`, - ); - - const { flatObjectMetadataMaps, flatFieldMetadataMaps } = - await this.workspaceCacheService.getOrRecompute(workspaceId, [ - 'flatObjectMetadataMaps', - 'flatFieldMetadataMaps', - ]); - - const attachmentObjectMetadata = - findFlatEntityByUniversalIdentifier({ - flatEntityMaps: flatObjectMetadataMaps, - universalIdentifier: STANDARD_OBJECTS.attachment.universalIdentifier, - }); - - if (!isDefined(attachmentObjectMetadata)) { - this.logger.warn( - `Attachment object metadata not found for workspace ${workspaceId}, skipping`, - ); - - return; - } - - const { twentyStandardFlatApplication } = - await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow( - { - workspaceId, - }, - ); - - let attachmentFileflatFieldMetadata = findFlatEntityByUniversalIdentifier({ - flatEntityMaps: flatFieldMetadataMaps, - universalIdentifier: - STANDARD_OBJECTS.attachment.fields.file.universalIdentifier, - }); - - if (!isDefined(attachmentFileflatFieldMetadata)) { - this.logger.log( - `File field metadata not found for attachments in workspace ${workspaceId}, creating it`, - ); - - if (!isDryRun) { - const attachmentFieldMetadatas = getFlatFieldsFromFlatObjectMetadata( - attachmentObjectMetadata, - flatFieldMetadataMaps, - ); - - const existingFieldWithSameName = attachmentFieldMetadatas.find( - (fieldMetadata) => fieldMetadata.name === 'file', - ); - - if (isDefined(existingFieldWithSameName)) { - this.logger.log( - `Found existing field with name 'file' (id: ${existingFieldWithSameName.id}), renaming it to 'old-file'`, - ); - - try { - await this.fieldMetadataService.updateOneField({ - updateFieldInput: { - id: existingFieldWithSameName.id, - name: 'oldFile', - label: 'Old File', - }, - workspaceId, - ownerFlatApplication: twentyStandardFlatApplication, - }); - } catch (error) { - this.logger.error( - `Failed to rename existing 'file' field to 'old-file' for workspace ${workspaceId}: ${error.message}`, - ); - throw error; - } - - this.logger.log( - `Renamed existing 'file' field to 'oldFile' for workspace ${workspaceId}`, - ); - } - - try { - attachmentFileflatFieldMetadata = - await this.fieldMetadataService.createOneField({ - createFieldInput: { - objectMetadataId: attachmentObjectMetadata.id, - type: FieldMetadataType.FILES, - name: 'file', - label: 'File', - description: 'Attachment file', - icon: 'IconFileUpload', - isNullable: true, - isUIReadOnly: true, - isSystem: true, - settings: { - maxNumberOfValues: 1, - }, - universalIdentifier: - STANDARD_OBJECTS.attachment.fields.file.universalIdentifier, - }, - workspaceId, - ownerFlatApplication: twentyStandardFlatApplication, - }); - } catch (error) { - this.logger.error( - `Failed to create file field metadata for attachments in workspace ${workspaceId}: ${error.message}`, - ); - throw error; - } - - this.logger.log( - `Created file field metadata for attachments in workspace ${workspaceId}`, - ); - } else { - this.logger.log( - `[DRY RUN] Would create file field metadata for attachments in workspace ${workspaceId}`, - ); - } - } - - if (!isDefined(attachmentFileflatFieldMetadata)) { - this.logger.error( - `File field metadata not found for attachments in workspace ${workspaceId}`, - ); - - return; - } - - const systemAuthContext = buildSystemAuthContext(workspaceId); - - await this.twentyORMGlobalManager.executeInWorkspaceContext(async () => { - const attachmentRepository = - await this.twentyORMGlobalManager.getRepository( - workspaceId, - 'attachment', - { shouldBypassPermissionChecks: true }, - ); - - const attachments = await attachmentRepository.find({ - where: { - fullPath: Not(IsNull()), - file: Or(IsNull(), Equal([])), - }, - select: ['id', 'name', 'fullPath'], - }); - - if (attachments.length === 0) { - this.logger.log( - `No attachments to migrate for workspace ${workspaceId}`, - ); - - return; - } - - this.logger.log( - `Found ${attachments.length} attachment(s) to migrate in workspace ${workspaceId}`, - ); - - const fileRepository = this.coreDataSource.getRepository(FileEntity); - - for (const attachment of attachments) { - if (!isNonEmptyString(attachment.fullPath)) { - this.logger.warn( - `Skipping attachment ${attachment.id} - invalid fullPath`, - ); - - continue; - } - - try { - const { type: fileExtension, filename } = - extractFolderPathFilenameAndTypeOrThrow(attachment.fullPath); - - const fileId = v4(); - const newFilename = `${fileId}${isNonEmptyString(fileExtension) ? `.${fileExtension}` : ''}`; - const newResourcePath = `${FileFolder.FilesField}/${attachmentFileflatFieldMetadata.universalIdentifier}/${newFilename}`; - - if (!isDryRun) { - await this.fileStorageService.copyLegacy({ - from: { - folderPath: `workspace-${workspaceId}`, - filename: attachment.fullPath, - }, - to: { - folderPath: `${workspaceId}/${twentyStandardFlatApplication.universalIdentifier}`, - filename: newResourcePath, - }, - }); - - const fileEntity = fileRepository.create({ - id: fileId, - path: newResourcePath, - workspaceId, - applicationId: twentyStandardFlatApplication.id, - size: -1, - settings: { - isTemporaryFile: true, - toDelete: false, - }, - }); - - await fileRepository.save(fileEntity); - - await attachmentRepository.update( - { id: attachment.id }, - { - file: [ - { - fileId: fileEntity.id, - label: attachment.name || filename, - }, - ], - }, - ); - } - - this.logger.log( - `Migrated attachment ${attachment.id} (${attachment.name})`, - ); - } catch (error) { - this.logger.error( - `Failed to migrate attachment ${attachment.id} in workspace ${workspaceId}: ${error.message}`, - ); - throw error; - } - } - - this.logger.log( - `${isDryRun ? '[DRY RUN] ' : ''}Completed attachment files migration for workspace ${workspaceId}`, - ); - }, systemAuthContext); - }; -} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-18/1-18-migrate-favorites-to-navigation-menu-items.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-18/1-18-migrate-favorites-to-navigation-menu-items.command.ts deleted file mode 100644 index 8f89789f4a..0000000000 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-18/1-18-migrate-favorites-to-navigation-menu-items.command.ts +++ /dev/null @@ -1,782 +0,0 @@ -import { Logger } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; - -import { Command } from 'nest-commander'; -import { STANDARD_OBJECTS } from 'twenty-shared/metadata'; -import { FieldMetadataType, FeatureFlagKey } from 'twenty-shared/types'; -import { isDefined } from 'twenty-shared/utils'; -import { In, IsNull, Not, Or, Repository } from 'typeorm'; -import { v4 as uuidv4 } from 'uuid'; - -import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner'; -import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner'; -import { ApplicationService } from 'src/engine/core-modules/application/application.service'; -import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; -import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service'; -import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; -import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; -import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; -import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util'; -import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.util'; -import { findManyFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; -import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; -import { isFlatFieldMetadataOfType } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-of-type.util'; -import { FlatNavigationMenuItemMaps } from 'src/engine/metadata-modules/flat-navigation-menu-item/types/flat-navigation-menu-item-maps.type'; -import { FlatNavigationMenuItem } from 'src/engine/metadata-modules/flat-navigation-menu-item/types/flat-navigation-menu-item.type'; -import { NavigationMenuItemType } from 'src/engine/metadata-modules/navigation-menu-item/enums/navigation-menu-item-type.enum'; -import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; -import { FlatView } from 'src/engine/metadata-modules/flat-view/types/flat-view.type'; -import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; -import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; -import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; -import { STANDARD_NAVIGATION_MENU_ITEMS } from 'src/engine/workspace-manager/twenty-standard-application/constants/standard-navigation-menu-item.constant'; -import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service'; -import { FavoriteFolderWorkspaceEntity } from 'src/modules/favorite-folder/standard-objects/favorite-folder.workspace-entity'; -import { FavoriteWorkspaceEntity } from 'src/modules/favorite/standard-objects/favorite.workspace-entity'; -import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; - -const WORKSPACE_LEVEL = 'workspace-level'; - -@Command({ - name: 'upgrade:1-18:migrate-favorites-to-navigation-menu-items', - description: 'Migrate favorites and favoriteFolders to navigationMenuItems', -}) -export class MigrateFavoritesToNavigationMenuItemsCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner { - protected readonly logger = new Logger( - MigrateFavoritesToNavigationMenuItemsCommand.name, - ); - - constructor( - @InjectRepository(WorkspaceEntity) - protected readonly workspaceRepository: Repository, - @InjectRepository(ObjectMetadataEntity) - private readonly objectMetadataRepository: Repository, - @InjectRepository(FieldMetadataEntity) - private readonly fieldMetadataRepository: Repository, - protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager, - protected readonly dataSourceService: DataSourceService, - private readonly applicationService: ApplicationService, - private readonly workspaceCacheService: WorkspaceCacheService, - private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService, - private readonly userWorkspaceService: UserWorkspaceService, - private readonly featureFlagService: FeatureFlagService, - ) { - super(workspaceRepository, twentyORMGlobalManager, dataSourceService); - } - - override async runOnWorkspace({ - workspaceId, - options, - }: RunOnWorkspaceArgs): Promise { - this.logger.log( - `Starting migration of favorites to navigation menu items for workspace ${workspaceId}`, - ); - - const isNavigationMenuItemEnabled = - await this.featureFlagService.isFeatureEnabled( - FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_ENABLED, - workspaceId, - ); - - if (isNavigationMenuItemEnabled) { - this.logger.log( - `Navigation menu item feature flag is already enabled for workspace ${workspaceId}. Skipping migration.`, - ); - - return; - } - - if (options.dryRun) { - this.logger.log( - `[DRY RUN] Would migrate favorites to navigation menu items for workspace ${workspaceId}`, - ); - - return; - } - - const { twentyStandardFlatApplication, workspaceCustomFlatApplication } = - await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow( - { - workspaceId, - }, - ); - - const { - flatObjectMetadataMaps, - flatViewMaps, - flatNavigationMenuItemMaps: existingFlatNavigationMenuItemMaps, - flatFieldMetadataMaps, - } = await this.workspaceCacheService.getOrRecompute(workspaceId, [ - 'flatObjectMetadataMaps', - 'flatViewMaps', - 'flatNavigationMenuItemMaps', - 'flatFieldMetadataMaps', - ]); - - const { - folderIdMapping, - flatNavigationMenuItemsToCreate: folderNavItemsToCreate, - } = await this.migrateFavoriteFolders({ - workspaceId, - workspaceCustomApplicationId: workspaceCustomFlatApplication.id, - workspaceCustomApplicationUniversalIdentifier: - workspaceCustomFlatApplication.universalIdentifier, - existingFlatNavigationMenuItemMaps, - }); - - const { flatNavigationMenuItemsToCreate: favoriteNavItemsToCreate } = - await this.migrateFavorites({ - workspaceId, - twentyStandardApplicationId: twentyStandardFlatApplication.id, - twentyStandardApplicationUniversalIdentifier: - twentyStandardFlatApplication.universalIdentifier, - workspaceCustomApplicationId: workspaceCustomFlatApplication.id, - workspaceCustomApplicationUniversalIdentifier: - workspaceCustomFlatApplication.universalIdentifier, - flatObjectMetadataMaps, - flatViewMaps, - folderIdMapping, - existingFlatNavigationMenuItemMaps, - flatFieldMetadataMaps, - }); - - const allFlatNavigationMenuItemsToCreate = [ - ...folderNavItemsToCreate, - ...favoriteNavItemsToCreate, - ]; - - if (allFlatNavigationMenuItemsToCreate.length > 0) { - const { flatNavigationMenuItemMaps: existingFlatNavigationMenuItemMaps } = - await this.workspaceCacheService.getOrRecompute(workspaceId, [ - 'flatNavigationMenuItemMaps', - ]); - - const twentyStandardItems = allFlatNavigationMenuItemsToCreate.filter( - (item) => item.applicationId === twentyStandardFlatApplication.id, - ); - const workspaceCustomItems = allFlatNavigationMenuItemsToCreate.filter( - (item) => item.applicationId === workspaceCustomFlatApplication.id, - ); - - if (workspaceCustomItems.length > 0) { - await this.createNavigationMenuItems({ - workspaceId, - flatNavigationMenuItemsToCreate: workspaceCustomItems, - existingFlatNavigationMenuItemMaps, - applicationUniversalIdentifier: - workspaceCustomFlatApplication.universalIdentifier, - }); - } - - if (twentyStandardItems.length > 0) { - await this.createNavigationMenuItems({ - workspaceId, - flatNavigationMenuItemsToCreate: twentyStandardItems, - existingFlatNavigationMenuItemMaps, - applicationUniversalIdentifier: - twentyStandardFlatApplication.universalIdentifier, - }); - } - } - - await this.featureFlagService.upsertWorkspaceFeatureFlag({ - workspaceId, - featureFlag: FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_ENABLED, - value: true, - }); - - this.logger.log( - `Successfully migrated favorites to navigation menu items for workspace ${workspaceId}`, - ); - } - - private async migrateFavoriteFolders({ - workspaceId, - workspaceCustomApplicationId, - workspaceCustomApplicationUniversalIdentifier, - existingFlatNavigationMenuItemMaps, - }: { - workspaceId: string; - workspaceCustomApplicationId: string; - workspaceCustomApplicationUniversalIdentifier: string; - existingFlatNavigationMenuItemMaps: FlatNavigationMenuItemMaps; - }): Promise<{ - folderIdMapping: Map>; - flatNavigationMenuItemsToCreate: FlatNavigationMenuItem[]; - }> { - const existingFlatNavigationMenuItemIds = Object.keys( - existingFlatNavigationMenuItemMaps.universalIdentifierById, - ); - - const favoriteFolderRepository = - await this.twentyORMGlobalManager.getRepository( - workspaceId, - 'favoriteFolder', - { shouldBypassPermissionChecks: true }, - ); - - const favoriteFolders = await favoriteFolderRepository.find({ - where: { - deletedAt: IsNull(), - id: Not(In(existingFlatNavigationMenuItemIds)), - }, - order: { position: 'ASC' }, - relations: { - favorites: true, - }, - }); - - if (favoriteFolders.length === 0) { - return { - folderIdMapping: new Map(), - flatNavigationMenuItemsToCreate: [], - }; - } - - const folderIdMapping = new Map>(); // favoriteFolderId -> workspaceMemberId or workspace-level -> navigationMenuItemId - const flatNavigationMenuItemsToCreate: FlatNavigationMenuItem[] = []; - - for (const favoriteFolder of favoriteFolders) { - const workspaceMemberIdToNavigationMenuItemIdMap = new Map< - string, - string - >(); - - const workspaceMemberIdsWithFavoritesInFolder = new Set( - favoriteFolder.favorites - .map((favorite) => favorite.forWorkspaceMemberId) - .filter(isDefined), - ); - - const hasWorkspaceScopedFavoritesInFolder = favoriteFolder.favorites.some( - (favorite) => !isDefined(favorite.forWorkspaceMemberId), - ); - - const now = new Date().toISOString(); - - for (const [index, workspaceMemberId] of Array.from( - workspaceMemberIdsWithFavoritesInFolder, - ).entries()) { - const navigationMenuItemId = index === 0 ? favoriteFolder.id : uuidv4(); - - workspaceMemberIdToNavigationMenuItemIdMap.set( - workspaceMemberId, - navigationMenuItemId, - ); - - const userWorkspaceId = - await this.getUserWorkspaceIdFromWorkspaceMemberId( - workspaceMemberId, - workspaceId, - ); - - const folderToCreate = { - id: navigationMenuItemId, - type: NavigationMenuItemType.FOLDER, - universalIdentifier: navigationMenuItemId, - userWorkspaceId, - targetRecordId: null, - targetObjectMetadataId: null, - targetObjectMetadataUniversalIdentifier: null, - viewId: null, - viewUniversalIdentifier: null, - folderId: null, - folderUniversalIdentifier: null, - name: favoriteFolder.name, - link: null, - icon: null, - color: null, - position: favoriteFolder.position, - workspaceId, - applicationId: workspaceCustomApplicationId, - applicationUniversalIdentifier: - workspaceCustomApplicationUniversalIdentifier, - createdAt: now, - updatedAt: now, - }; - - flatNavigationMenuItemsToCreate.push(folderToCreate); - } - - if (hasWorkspaceScopedFavoritesInFolder) { - const workspaceLevelNavigationMenuItemId = uuidv4(); - - workspaceMemberIdToNavigationMenuItemIdMap.set( - WORKSPACE_LEVEL, - workspaceLevelNavigationMenuItemId, - ); - - const workspaceLevelFolderToCreate = { - id: workspaceLevelNavigationMenuItemId, - type: NavigationMenuItemType.FOLDER, - universalIdentifier: workspaceLevelNavigationMenuItemId, - userWorkspaceId: null, - targetRecordId: null, - targetObjectMetadataId: null, - targetObjectMetadataUniversalIdentifier: null, - viewId: null, - viewUniversalIdentifier: null, - folderId: null, - folderUniversalIdentifier: null, - name: favoriteFolder.name, - link: null, - icon: null, - color: null, - position: favoriteFolder.position, - workspaceId, - applicationId: workspaceCustomApplicationId, - applicationUniversalIdentifier: - workspaceCustomApplicationUniversalIdentifier, - createdAt: now, - updatedAt: now, - }; - - flatNavigationMenuItemsToCreate.push(workspaceLevelFolderToCreate); - } - - folderIdMapping.set( - favoriteFolder.id, - workspaceMemberIdToNavigationMenuItemIdMap, - ); - } - - return { - folderIdMapping, - flatNavigationMenuItemsToCreate, - }; - } - - private async migrateFavorites({ - workspaceId, - twentyStandardApplicationId, - twentyStandardApplicationUniversalIdentifier, - workspaceCustomApplicationId, - workspaceCustomApplicationUniversalIdentifier, - flatObjectMetadataMaps, - flatViewMaps, - folderIdMapping, - existingFlatNavigationMenuItemMaps, - flatFieldMetadataMaps, - }: { - workspaceId: string; - twentyStandardApplicationId: string; - twentyStandardApplicationUniversalIdentifier: string; - workspaceCustomApplicationId: string; - workspaceCustomApplicationUniversalIdentifier: string; - flatObjectMetadataMaps: FlatEntityMaps; - flatViewMaps: FlatEntityMaps; - folderIdMapping: Map>; - existingFlatNavigationMenuItemMaps: FlatNavigationMenuItemMaps; - flatFieldMetadataMaps: FlatEntityMaps; - }): Promise<{ - migratedFavoriteIds: string[]; - flatNavigationMenuItemsToCreate: FlatNavigationMenuItem[]; - }> { - const favoriteRepository = - await this.twentyORMGlobalManager.getRepository( - workspaceId, - 'favorite', - { shouldBypassPermissionChecks: true }, - ); - - const existingFlatNavigationMenuItemIds = Object.keys( - existingFlatNavigationMenuItemMaps.universalIdentifierById, - ); - - const viewIdsFromExistingNavigationMenuItems = Object.values( - existingFlatNavigationMenuItemMaps.byUniversalIdentifier, - ) - .filter(isDefined) - .map((item): string | null => item.viewId) - .filter(isDefined); - - const favorites = await favoriteRepository.find({ - where: { - deletedAt: IsNull(), - id: Not(In(existingFlatNavigationMenuItemIds)), - viewId: Or(IsNull(), Not(In(viewIdsFromExistingNavigationMenuItems))), - }, - order: { position: 'ASC' }, - }); - - if (favorites.length === 0) { - return { migratedFavoriteIds: [], flatNavigationMenuItemsToCreate: [] }; - } - - const favoriteObjectMetadata = findFlatEntityByUniversalIdentifier({ - flatEntityMaps: flatObjectMetadataMaps, - universalIdentifier: STANDARD_OBJECTS.favorite.universalIdentifier, - }); - - if (!isDefined(favoriteObjectMetadata)) { - throw new Error('Favorite object metadata not found'); - } - - const favoriteRelationFields = - findManyFlatEntityByIdInFlatEntityMapsOrThrow({ - flatEntityMaps: flatFieldMetadataMaps, - flatEntityIds: favoriteObjectMetadata.fieldIds, - }).filter( - (field): field is FlatFieldMetadata => - isFlatFieldMetadataOfType(field, FieldMetadataType.RELATION), - ); - - const favoriteRelationFieldMap = new Map< - string, - { name: string; relationTargetUniversalIdentifier: string } - >(); - - for (const field of favoriteRelationFields) { - if ( - field.name !== 'forWorkspaceMember' && - field.name !== 'favoriteFolder' && - isDefined(field.relationTargetObjectMetadataId) - ) { - const targetObjectMetadata = findFlatEntityByIdInFlatEntityMaps({ - flatEntityMaps: flatObjectMetadataMaps, - flatEntityId: field.relationTargetObjectMetadataId, - }); - - if (isDefined(targetObjectMetadata)) { - favoriteRelationFieldMap.set(field.name, { - name: field.name, - relationTargetUniversalIdentifier: - targetObjectMetadata.universalIdentifier, - }); - } - } - } - - const flatNavigationMenuItemsToCreate: FlatNavigationMenuItem[] = []; - const migratedFavoriteIds: string[] = []; - - for (const favorite of favorites) { - const userWorkspaceId = !isDefined(favorite.forWorkspaceMemberId) - ? null - : await this.getUserWorkspaceIdFromWorkspaceMemberId( - favorite.forWorkspaceMemberId, - workspaceId, - ); - - if ( - isDefined(favorite.forWorkspaceMemberId) && - !isDefined(userWorkspaceId) - ) { - this.logger.warn( - `Skipping favorite ${favorite.id} - forWorkspaceMemberId ${favorite.forWorkspaceMemberId} did not resolve to a userWorkspaceId`, - ); - - continue; - } - - const folderId = favorite.favoriteFolderId - ? (folderIdMapping - .get(favorite.favoriteFolderId) - ?.get( - isDefined(favorite.forWorkspaceMemberId) - ? favorite.forWorkspaceMemberId - : WORKSPACE_LEVEL, - ) ?? null) - : null; - - if (isDefined(favorite.viewId)) { - const existingItem = Object.values( - existingFlatNavigationMenuItemMaps.byUniversalIdentifier, - ).find( - (item): item is FlatNavigationMenuItem => - isDefined(item) && - item.workspaceId === workspaceId && - item.userWorkspaceId === userWorkspaceId && - item.viewId === favorite.viewId && - item.folderId === folderId, - ); - - if (isDefined(existingItem)) { - this.logger.log( - `Skipping favorite ${favorite.id} - navigation menu item already exists for view ${favorite.viewId}`, - ); - - continue; - } - - try { - const { - applicationId, - applicationUniversalIdentifier, - universalIdentifier, - } = this.getApplicationIdAndUniversalIdentifierForViewFavorite({ - viewId: favorite.viewId, - flatViewMaps, - twentyStandardApplicationId, - twentyStandardApplicationUniversalIdentifier, - workspaceCustomApplicationId, - workspaceCustomApplicationUniversalIdentifier, - }); - - const now = new Date().toISOString(); - - flatNavigationMenuItemsToCreate.push({ - id: favorite.id, - type: NavigationMenuItemType.VIEW, - universalIdentifier, - userWorkspaceId, - targetRecordId: null, - targetObjectMetadataId: null, - targetObjectMetadataUniversalIdentifier: null, - viewId: favorite.viewId, - viewUniversalIdentifier: - flatViewMaps.universalIdentifierById[favorite.viewId] ?? null, - folderId, - folderUniversalIdentifier: folderId, - name: null, - link: null, - icon: null, - color: null, - position: favorite.position, - workspaceId, - applicationId, - applicationUniversalIdentifier, - createdAt: now, - updatedAt: now, - }); - - migratedFavoriteIds.push(favorite.id); - } catch (error) { - this.logger.error( - `Failed to migrate favorite ${favorite.id} with view ${favorite.viewId} - ${error}`, - ); - - continue; - } - - continue; - } - - const { targetRecordId, targetObjectMetadataId } = - this.getTargetRecordFromFavorite( - favorite, - flatObjectMetadataMaps, - favoriteRelationFieldMap, - ); - - if (!isDefined(targetRecordId) || !isDefined(targetObjectMetadataId)) { - this.logger.warn( - `Skipping favorite ${favorite.id} - no valid target record found`, - ); - - continue; - } - - const now = new Date().toISOString(); - - flatNavigationMenuItemsToCreate.push({ - id: favorite.id, - type: NavigationMenuItemType.RECORD, - universalIdentifier: favorite.id, - userWorkspaceId, - targetRecordId, - targetObjectMetadataId, - targetObjectMetadataUniversalIdentifier: - flatObjectMetadataMaps.universalIdentifierById[ - targetObjectMetadataId - ] ?? null, - viewId: null, - viewUniversalIdentifier: null, - folderId, - folderUniversalIdentifier: folderId, - name: null, - link: null, - icon: null, - color: null, - position: favorite.position, - workspaceId, - applicationId: workspaceCustomApplicationId, - applicationUniversalIdentifier: - workspaceCustomApplicationUniversalIdentifier, - createdAt: now, - updatedAt: now, - }); - - migratedFavoriteIds.push(favorite.id); - } - - return { - migratedFavoriteIds, - flatNavigationMenuItemsToCreate, - }; - } - - private getApplicationIdAndUniversalIdentifierForViewFavorite({ - viewId, - flatViewMaps, - twentyStandardApplicationId, - twentyStandardApplicationUniversalIdentifier, - workspaceCustomApplicationId, - workspaceCustomApplicationUniversalIdentifier, - }: { - viewId: string; - flatViewMaps: FlatEntityMaps; - twentyStandardApplicationId: string; - twentyStandardApplicationUniversalIdentifier: string; - workspaceCustomApplicationId: string; - workspaceCustomApplicationUniversalIdentifier: string; - }): { - applicationId: string; - applicationUniversalIdentifier: string; - universalIdentifier: string; - } { - const flatView = findFlatEntityByIdInFlatEntityMaps({ - flatEntityMaps: flatViewMaps, - flatEntityId: viewId, - }); - - if (!isDefined(flatView)) { - throw new Error(`View ${viewId} not found`); - } - - const matchingStandardItem = Object.values( - STANDARD_NAVIGATION_MENU_ITEMS, - ).find( - (item) => - 'viewUniversalIdentifier' in item && - item.viewUniversalIdentifier === flatView.universalIdentifier, - ); - - return !isDefined(matchingStandardItem) - ? { - applicationId: workspaceCustomApplicationId, - applicationUniversalIdentifier: - workspaceCustomApplicationUniversalIdentifier, - universalIdentifier: uuidv4(), - } - : { - applicationId: twentyStandardApplicationId, - applicationUniversalIdentifier: - twentyStandardApplicationUniversalIdentifier, - universalIdentifier: matchingStandardItem.universalIdentifier, - }; - } - - private async getUserWorkspaceIdFromWorkspaceMemberId( - workspaceMemberId: string | null | undefined, - workspaceId: string, - ): Promise { - if (!isDefined(workspaceMemberId)) { - return null; - } - const authContext = buildSystemAuthContext(workspaceId); - - try { - const workspaceMember = - await this.twentyORMGlobalManager.executeInWorkspaceContext( - async () => { - const workspaceMemberRepository = - await this.twentyORMGlobalManager.getRepository( - workspaceId, - 'workspaceMember', - { shouldBypassPermissionChecks: true }, - ); - - return await workspaceMemberRepository.findOne({ - where: { id: workspaceMemberId }, - }); - }, - authContext, - ); - - if (!isDefined(workspaceMember)) { - return null; - } - - const userWorkspace = - await this.userWorkspaceService.getUserWorkspaceForUserOrThrow({ - userId: workspaceMember.userId, - workspaceId, - }); - - return userWorkspace.id; - } catch (error) { - this.logger.warn( - `Failed to get userWorkspaceId for workspaceMemberId ${workspaceMemberId}: ${error}`, - ); - - return null; - } - } - - private getTargetRecordFromFavorite( - favorite: FavoriteWorkspaceEntity, - flatObjectMetadataMaps: FlatEntityMaps, - favoriteRelationFieldMap: Map< - string, - { name: string; relationTargetUniversalIdentifier: string } - >, - ): { - targetRecordId: string | null; - targetObjectMetadataId: string | null; - } { - for (const [ - fieldName, - { relationTargetUniversalIdentifier }, - ] of favoriteRelationFieldMap.entries()) { - const fieldIdName = `${fieldName}Id`; - const recordId = ( - favorite as unknown as Record - )[fieldIdName]; - - if (!isDefined(recordId)) { - continue; - } - - const targetObjectMetadata = findFlatEntityByUniversalIdentifier({ - flatEntityMaps: flatObjectMetadataMaps, - universalIdentifier: relationTargetUniversalIdentifier, - }); - - if (isDefined(targetObjectMetadata)) { - return { - targetRecordId: recordId, - targetObjectMetadataId: targetObjectMetadata.id, - }; - } - } - - return { targetRecordId: null, targetObjectMetadataId: null }; - } - - private async createNavigationMenuItems({ - workspaceId, - flatNavigationMenuItemsToCreate, - existingFlatNavigationMenuItemMaps: _existingFlatNavigationMenuItemMaps, - applicationUniversalIdentifier, - }: { - workspaceId: string; - flatNavigationMenuItemsToCreate: FlatNavigationMenuItem[]; - existingFlatNavigationMenuItemMaps: FlatNavigationMenuItemMaps; - applicationUniversalIdentifier: string; - }): Promise { - const validateAndBuildResult = - await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( - { - allFlatEntityOperationByMetadataName: { - navigationMenuItem: { - flatEntityToCreate: flatNavigationMenuItemsToCreate, - flatEntityToDelete: [], - flatEntityToUpdate: [], - }, - }, - workspaceId, - isSystemBuild: true, - applicationUniversalIdentifier, - }, - ); - - if (validateAndBuildResult.status === 'fail') { - throw new Error( - `Failed to create navigation menu items: ${JSON.stringify(validateAndBuildResult, null, 2)}`, - ); - } - } -} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-18/1-18-migrate-person-avatar-files.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-18/1-18-migrate-person-avatar-files.command.ts deleted file mode 100644 index 5109b5af1f..0000000000 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-18/1-18-migrate-person-avatar-files.command.ts +++ /dev/null @@ -1,299 +0,0 @@ -import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; - -import { isNonEmptyString } from '@sniptt/guards'; -import { Command } from 'nest-commander'; -import { STANDARD_OBJECTS } from 'twenty-shared/metadata'; -import { FieldMetadataType, FileFolder } from 'twenty-shared/types'; -import { - assertIsDefinedOrThrow, - extractFolderPathFilenameAndTypeOrThrow, - isDefined, -} from 'twenty-shared/utils'; -import { DataSource, Equal, ILike, IsNull, Or, Repository } from 'typeorm'; -import { v4 } from 'uuid'; - -import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner'; -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 { ApplicationService } from 'src/engine/core-modules/application/application.service'; -import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service'; -import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity'; -import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; -import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; -import { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata.service'; -import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.util'; -import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; -import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; -import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; -import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; -import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity'; - -@Command({ - name: 'upgrade:1-18:migrate-person-avatar-files', - description: - '[DEPRECATED] Migrate person avatarUrl files to file field - this migration is now complete and no longer needed', -}) -export class MigratePersonAvatarFilesCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner { - constructor( - @InjectRepository(WorkspaceEntity) - protected readonly workspaceRepository: Repository, - protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager, - protected readonly dataSourceService: DataSourceService, - private readonly fileStorageService: FileStorageService, - private readonly workspaceCacheService: WorkspaceCacheService, - private readonly fieldMetadataService: FieldMetadataService, - private readonly applicationService: ApplicationService, - @InjectDataSource() - private readonly coreDataSource: DataSource, - ) { - super(workspaceRepository, twentyORMGlobalManager, dataSourceService); - } - - override async runOnWorkspace({ - workspaceId, - }: RunOnWorkspaceArgs): Promise { - this.logger.log( - `[DEPRECATED] Person avatar files migration is no longer needed for workspace ${workspaceId}. ` + - `The IS_FILES_FIELD_MIGRATED feature flag has been removed as all workspaces are now migrated.`, - ); - } - - private _deprecatedMigrationLogic = async ({ - workspaceId, - isDryRun, - }: { - workspaceId: string; - isDryRun: boolean; - }) => { - this.logger.log( - `${ - isDryRun ? '[DRY RUN] ' : '' - }Starting person avatar files migration for workspace ${workspaceId}`, - ); - - const { flatObjectMetadataMaps, flatFieldMetadataMaps } = - await this.workspaceCacheService.getOrRecompute(workspaceId, [ - 'flatObjectMetadataMaps', - 'flatFieldMetadataMaps', - ]); - - const personObjectMetadata = - findFlatEntityByUniversalIdentifier({ - flatEntityMaps: flatObjectMetadataMaps, - universalIdentifier: STANDARD_OBJECTS.person.universalIdentifier, - }); - - if (!isDefined(personObjectMetadata)) { - this.logger.warn( - `Person object metadata not found for workspace ${workspaceId}, skipping`, - ); - - return; - } - - const { twentyStandardFlatApplication } = - await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow( - { - workspaceId, - }, - ); - - if (!isDefined(twentyStandardFlatApplication)) { - this.logger.error( - `Twenty standard application not found for workspace ${workspaceId}`, - ); - - return; - } - - let avatarFileFieldMetadata = findFlatEntityByUniversalIdentifier({ - flatEntityMaps: flatFieldMetadataMaps, - universalIdentifier: - STANDARD_OBJECTS.person.fields.avatarFile.universalIdentifier, - }); - - if (!isDefined(avatarFileFieldMetadata)) { - this.logger.log( - `Avatar file field metadata not found for workspace ${workspaceId}, creating it`, - ); - - if (!isDryRun) { - const personFieldMetadatas = getFlatFieldsFromFlatObjectMetadata( - personObjectMetadata, - flatFieldMetadataMaps, - ); - - const existingFieldWithSameName = personFieldMetadatas.find( - (fieldMetadata) => fieldMetadata.name === 'avatarFile', - ); - - if (isDefined(existingFieldWithSameName)) { - this.logger.log( - `Found existing field with name 'avatarFile' (id: ${existingFieldWithSameName.id}), renaming it to 'old-avatarFile'`, - ); - - try { - await this.fieldMetadataService.updateOneField({ - updateFieldInput: { - id: existingFieldWithSameName.id, - name: 'oldAvatarFile', - label: 'Old Avatar File', - }, - workspaceId, - ownerFlatApplication: twentyStandardFlatApplication, - }); - } catch (error) { - this.logger.error( - `Failed to rename existing 'avatarFile' field to 'old-avatarFile' for workspace ${workspaceId}: ${error.message}`, - ); - throw error; - } - - this.logger.log( - `Renamed existing 'avatarFile' field to 'oldAvatarFile' for workspace ${workspaceId}`, - ); - } - - try { - avatarFileFieldMetadata = - await this.fieldMetadataService.createOneField({ - createFieldInput: { - objectMetadataId: personObjectMetadata.id, - type: FieldMetadataType.FILES, - name: 'avatarFile', - label: 'Avatar File', - description: "Contact's avatar file", - icon: 'IconFileUpload', - isSystem: true, - isNullable: true, - settings: { - maxNumberOfValues: 1, - }, - universalIdentifier: - STANDARD_OBJECTS.person.fields.avatarFile.universalIdentifier, - }, - workspaceId, - ownerFlatApplication: twentyStandardFlatApplication, - }); - } catch (error) { - this.logger.error( - `Failed to create avatarFile field metadata for workspace ${workspaceId}: ${error.message}`, - ); - throw error; - } - - this.logger.log( - `Created avatarFile field metadata for workspace ${workspaceId}`, - ); - } else { - this.logger.log( - `[DRY RUN] Would create avatarFile field metadata for workspace ${workspaceId}`, - ); - } - } - - if (!isDefined(avatarFileFieldMetadata)) { - this.logger.error( - `Avatar file field metadata not found for workspace ${workspaceId}`, - ); - - return; - } - - const systemAuthContext = buildSystemAuthContext(workspaceId); - - await this.twentyORMGlobalManager.executeInWorkspaceContext(async () => { - const personRepository = - await this.twentyORMGlobalManager.getRepository( - workspaceId, - 'person', - { shouldBypassPermissionChecks: true }, - ); - - const personsWithAvatars = await personRepository.find({ - where: { - avatarUrl: ILike(`%${FileFolder.PersonPicture}%`), - avatarFile: Or(IsNull(), Equal([])), - }, - select: ['id', 'avatarUrl'], - }); - - if (personsWithAvatars.length === 0) { - this.logger.log( - `No persons with avatarUrl found in workspace ${workspaceId}`, - ); - - return; - } - - this.logger.log( - `Found ${personsWithAvatars.length} person(s) with avatarUrl containing 'people' folder in workspace ${workspaceId}`, - ); - - const fileRepository = this.coreDataSource.getRepository(FileEntity); - - for (const person of personsWithAvatars) { - assertIsDefinedOrThrow(person.avatarUrl); - - try { - const { type: fileExtension } = - extractFolderPathFilenameAndTypeOrThrow(person.avatarUrl); - - const fileId = v4(); - const newFileName = `${fileId}${isNonEmptyString(fileExtension) ? `.${fileExtension}` : ''}`; - const newResourcePath = `${FileFolder.FilesField}/${avatarFileFieldMetadata.universalIdentifier}/${newFileName}`; - - if (!isDryRun) { - await this.fileStorageService.copyLegacy({ - from: { - folderPath: `workspace-${workspaceId}`, - filename: person.avatarUrl, - }, - to: { - folderPath: `${workspaceId}/${twentyStandardFlatApplication.universalIdentifier}`, - filename: newResourcePath, - }, - }); - - const fileEntity = fileRepository.create({ - id: fileId, - path: newResourcePath, - workspaceId, - applicationId: twentyStandardFlatApplication.id, - size: -1, - settings: { - isTemporaryFile: true, - toDelete: false, - }, - }); - - await fileRepository.save(fileEntity); - - await personRepository.update( - { id: person.id }, - { - avatarFile: [ - { - fileId: fileEntity.id, - label: newFileName, - }, - ], - }, - ); - } - - this.logger.log(`Migrated avatar for person ${person.id}`); - } catch (error) { - this.logger.error( - `Failed to migrate avatar for person ${person.id} in workspace ${workspaceId}: ${error.message}`, - ); - throw error; - } - } - - this.logger.log( - `${isDryRun ? '[DRY RUN] ' : ''}Completed person avatar files migration for workspace ${workspaceId}`, - ); - }, systemAuthContext); - }; -} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-18/1-18-migrate-workflow-send-email-attachments.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-18/1-18-migrate-workflow-send-email-attachments.command.ts deleted file mode 100644 index 9431e394a5..0000000000 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-18/1-18-migrate-workflow-send-email-attachments.command.ts +++ /dev/null @@ -1,211 +0,0 @@ -import { Logger } from '@nestjs/common'; -import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; - -import { isNonEmptyString } from '@sniptt/guards'; -import { Command } from 'nest-commander'; -import { FileFolder } from 'twenty-shared/types'; -import { isDefined, isNonEmptyArray } from 'twenty-shared/utils'; -import { DataSource, In, Repository } from 'typeorm'; - -import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner'; -import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner'; -import { ApplicationService } from 'src/engine/core-modules/application/application.service'; -import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service'; -import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity'; -import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; -import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; -import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; -import { - WorkflowVersionStatus, - WorkflowVersionWorkspaceEntity, -} from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity'; -import { WorkflowActionType } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action-type.enum'; - -type WorkflowFile = { - id: string; - path?: string; - name: string; - size: number; - type: string; - createdAt: string; -}; - -type SendEmailStep = { - id: string; - type: WorkflowActionType.SEND_EMAIL; - settings: { - input: { - files?: WorkflowFile[]; - [key: string]: unknown; - }; - [key: string]: unknown; - }; - [key: string]: unknown; -}; - -@Command({ - name: 'upgrade:1-18:migrate-workflow-send-email-attachments', - description: - '[DEPRECATED] Migrate workflow send email attachments - this migration is now complete and no longer needed', -}) -export class MigrateWorkflowSendEmailAttachmentsCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner { - protected readonly logger = new Logger( - MigrateWorkflowSendEmailAttachmentsCommand.name, - ); - - constructor( - @InjectRepository(WorkspaceEntity) - protected readonly workspaceRepository: Repository, - protected readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, - protected readonly dataSourceService: DataSourceService, - private readonly fileStorageService: FileStorageService, - private readonly applicationService: ApplicationService, - @InjectDataSource() - private readonly coreDataSource: DataSource, - ) { - super(workspaceRepository, globalWorkspaceOrmManager, dataSourceService); - } - - override async runOnWorkspace({ - workspaceId, - }: RunOnWorkspaceArgs): Promise { - this.logger.log( - `[DEPRECATED] Workflow send email attachments migration is no longer needed for workspace ${workspaceId}. ` + - `The IS_OTHER_FILE_MIGRATED feature flag has been removed as all workspaces are now migrated.`, - ); - } - - private _deprecatedMigrationLogic = async ({ - workspaceId, - isDryRun, - }: { - workspaceId: string; - isDryRun: boolean; - }) => { - const isMigrated = false; - - if (isMigrated) { - this.logger.log( - `Workflow attachments migration already completed for workspace ${workspaceId}, skipping`, - ); - - return; - } - - this.logger.log( - `${isDryRun ? '[DRY RUN] ' : ''}Starting workflow send email attachments migration for workspace ${workspaceId}`, - ); - - const workflowVersionRepository = - await this.globalWorkspaceOrmManager.getRepository( - workspaceId, - 'workflowVersion', - { shouldBypassPermissionChecks: true }, - ); - - const workflowVersions = await workflowVersionRepository.find({ - select: ['id', 'steps'], - where: { - status: In([ - WorkflowVersionStatus.DRAFT, - WorkflowVersionStatus.ACTIVE, - WorkflowVersionStatus.DEACTIVATED, - ]), - }, - }); - - const { workspaceCustomFlatApplication } = - await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow( - { workspaceId }, - ); - - const fileRepository = this.coreDataSource.getRepository(FileEntity); - - for (const workflowVersion of workflowVersions) { - const steps = workflowVersion.steps; - - if (!isNonEmptyArray(steps)) { - continue; - } - - for (const step of steps) { - if (step.type !== WorkflowActionType.SEND_EMAIL) { - continue; - } - - const sendEmailStep = step as SendEmailStep; - const files = sendEmailStep.settings?.input?.files; - - if (!isNonEmptyArray(files)) { - continue; - } - - for (const file of files) { - const fileEntity = await fileRepository.findOne({ - where: { - id: file.id, - workspaceId, - }, - }); - - if (!isDefined(fileEntity)) { - this.logger.warn( - `File ${file.id} not found for workflow version ${workflowVersion.id}, skipping`, - ); - continue; - } - - if (fileEntity.path.startsWith(FileFolder.Workflow)) { - this.logger.log( - `File ${file.id} already in Workflow folder, skipping copy`, - ); - continue; - } - - const newResourcePath = `${fileEntity.id}${isNonEmptyString(file.type) ? `.${file.type}` : ''}`; - const newPath = `${FileFolder.Workflow}/${newResourcePath}`; - - if (!isDryRun) { - try { - await this.fileStorageService.copyLegacy({ - from: { - folderPath: `workspace-${workspaceId}`, - filename: fileEntity.path, - }, - to: { - folderPath: `${workspaceId}/${workspaceCustomFlatApplication.universalIdentifier}`, - filename: newPath, - }, - }); - } catch (error) { - this.logger.error( - `Failed to migrate file ${fileEntity.id} in workspace ${workspaceId}: ${error.message}`, - ); - continue; - } - } else { - this.logger.log( - `[DRY RUN] Would migrate file ${fileEntity.id} from ${fileEntity.path} to ${newPath}`, - ); - } - - await fileRepository.update( - { id: fileEntity.id }, - { - path: newPath, - applicationId: workspaceCustomFlatApplication.id, - settings: { - isTemporaryFile: true, - toDelete: false, - }, - }, - ); - } - } - } - - this.logger.log( - `${isDryRun ? '[DRY RUN] ' : ''}Completed workflow send email attachments migration for workspace ${workspaceId}`, - ); - }; -} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-18/1-18-migrate-workspace-pictures.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-18/1-18-migrate-workspace-pictures.command.ts deleted file mode 100644 index 70f443aedb..0000000000 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-18/1-18-migrate-workspace-pictures.command.ts +++ /dev/null @@ -1,381 +0,0 @@ -import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; - -import { isNonEmptyString } from '@sniptt/guards'; -import { FileTypeParser } from 'file-type'; -import { detectPdf } from '@file-type/pdf'; -import { Command } from 'nest-commander'; -import { STANDARD_OBJECTS } from 'twenty-shared/metadata'; -import { FileFolder } from 'twenty-shared/types'; -import { - extractFolderPathFilenameAndTypeOrThrow, - isDefined, -} from 'twenty-shared/utils'; -import { And, DataSource, IsNull, Like, Not, Repository } from 'typeorm'; -import { v4 } from 'uuid'; - -import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner'; -import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner'; -import { ApplicationService } from 'src/engine/core-modules/application/application.service'; -import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type'; -import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service'; -import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity'; -import { FileUrlService } from 'src/engine/core-modules/file/file-url/file-url.service'; -import { SecureHttpClientService } from 'src/engine/core-modules/secure-http-client/secure-http-client.service'; -import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; -import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; -import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.util'; -import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; -import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; -import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; -import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; -import { getImageBufferFromUrl } from 'src/utils/image'; - -@Command({ - name: 'upgrade:1-18:migrate-workspace-pictures', - description: - '[DEPRECATED] Migrate workspace logos and workspace member avatars to file records - this migration is now complete and no longer needed', -}) -export class MigrateWorkspacePicturesCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner { - constructor( - @InjectRepository(WorkspaceEntity) - protected readonly workspaceRepository: Repository, - protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager, - protected readonly dataSourceService: DataSourceService, - private readonly fileStorageService: FileStorageService, - private readonly workspaceCacheService: WorkspaceCacheService, - private readonly applicationService: ApplicationService, - private readonly fileUrlService: FileUrlService, - private readonly secureHttpClientService: SecureHttpClientService, - @InjectDataSource() - private readonly coreDataSource: DataSource, - ) { - super(workspaceRepository, twentyORMGlobalManager, dataSourceService); - } - - override async runOnWorkspace({ - workspaceId, - }: RunOnWorkspaceArgs): Promise { - this.logger.log( - `[DEPRECATED] Workspace pictures migration is no longer needed for workspace ${workspaceId}. ` + - `The IS_CORE_PICTURE_MIGRATED feature flag has been removed as all workspaces are now migrated.`, - ); - } - - private async migrateWorkspaceLogo({ - workspaceId, - isDryRun, - workspaceCustomFlatApplication, - fileRepository, - }: { - workspaceId: string; - isDryRun: boolean; - workspaceCustomFlatApplication: FlatApplication; - fileRepository: Repository; - }): Promise { - const workspace = await this.workspaceRepository.findOne({ - where: { - id: workspaceId, - logo: Not(IsNull()), - logoFileId: IsNull(), - }, - }); - - if (!workspace || !isNonEmptyString(workspace.logo)) { - this.logger.log( - `No workspace logo to migrate for workspace ${workspaceId}`, - ); - - return; - } - - const isInWorkspaceLogo = workspace.logo.startsWith( - FileFolder.WorkspaceLogo, - ); - - const isTwentyIconLogo = workspace.logo.includes('twenty-icons'); - - if (!isTwentyIconLogo && !isInWorkspaceLogo) { - this.logger.log( - `Workspace logo is not a twenty icon or a workspace logo, skipping`, - ); - - return; - } - - this.logger.log( - `Migrating workspace logo for workspace ${workspaceId}: ${workspace.logo}`, - ); - - if (isInWorkspaceLogo) { - await this.migrateWorkspaceLogoFromWorkspaceFolder({ - workspaceId, - logoPath: workspace.logo, - isDryRun, - workspaceCustomFlatApplication, - fileRepository, - }); - } - - if (isTwentyIconLogo) { - await this.migrateWorkspaceLogoFromTwentyIcons({ - workspaceId, - logoUrl: workspace.logo, - isDryRun, - workspaceCustomFlatApplication, - }); - } - } - - private async migrateWorkspaceLogoFromWorkspaceFolder({ - workspaceId, - logoPath, - isDryRun, - workspaceCustomFlatApplication, - fileRepository, - }: { - workspaceId: string; - logoPath: string; - isDryRun: boolean; - workspaceCustomFlatApplication: FlatApplication; - fileRepository: Repository; - }): Promise { - try { - const { type: fileExtension } = - extractFolderPathFilenameAndTypeOrThrow(logoPath); - - const fileId = v4(); - const newFilename = `${fileId}${isNonEmptyString(fileExtension) ? `.${fileExtension}` : ''}`; - const newResourcePath = `${FileFolder.CorePicture}/${newFilename}`; - - if (!isDryRun) { - await this.fileStorageService.copyLegacy({ - from: { - folderPath: `workspace-${workspaceId}`, - filename: logoPath, - }, - to: { - folderPath: `${workspaceId}/${workspaceCustomFlatApplication.universalIdentifier}`, - filename: newResourcePath, - }, - }); - - const fileEntity = fileRepository.create({ - id: fileId, - path: newResourcePath, - workspaceId, - applicationId: workspaceCustomFlatApplication.id, - size: -1, - settings: { - isTemporaryFile: false, - toDelete: false, - }, - }); - - await fileRepository.save(fileEntity); - - await this.workspaceRepository.update( - { id: workspaceId }, - { logoFileId: fileId }, - ); - } - - this.logger.log( - `Migrated workspace logo for workspace ${workspaceId} (${logoPath} -> ${newResourcePath})`, - ); - } catch (error) { - this.logger.error( - `Failed to migrate workspace logo for workspace ${workspaceId}: ${error.message}`, - ); - throw error; - } - } - - private async migrateWorkspaceLogoFromTwentyIcons({ - workspaceId, - logoUrl, - isDryRun, - workspaceCustomFlatApplication, - }: { - workspaceId: string; - logoUrl: string; - isDryRun: boolean; - workspaceCustomFlatApplication: FlatApplication; - }): Promise { - try { - const httpClient = this.secureHttpClientService.getHttpClient(); - const buffer = await getImageBufferFromUrl(logoUrl, httpClient); - - const parser = new FileTypeParser({ customDetectors: [detectPdf] }); - const type = await parser.fromBuffer(buffer); - - if (!isDefined(type) || !type.mime.startsWith('image/')) { - this.logger.warn( - `Unable to detect image type for workspace logo ${logoUrl}, skipping`, - ); - - return; - } - - const fileId = v4(); - const newFilename = `${fileId}.${type.ext}`; - const newResourcePath = `${newFilename}`; - - if (!isDryRun) { - const fileEntity = await this.fileStorageService.writeFile({ - workspaceId, - applicationUniversalIdentifier: - workspaceCustomFlatApplication.universalIdentifier, - fileFolder: FileFolder.CorePicture, - resourcePath: newResourcePath, - sourceFile: buffer, - mimeType: type.mime, - fileId, - settings: { - isTemporaryFile: false, - toDelete: false, - }, - }); - - await this.workspaceRepository.update( - { id: workspaceId }, - { logoFileId: fileEntity.id }, - ); - } - - this.logger.log( - `Migrated workspace logo from twenty-icons for workspace ${workspaceId} (${logoUrl} -> ${FileFolder.CorePicture}/${newResourcePath})`, - ); - } catch (error) { - this.logger.error( - `Failed to migrate workspace logo from twenty-icons for workspace ${workspaceId}: ${error.message}`, - ); - } - } - - private async migrateWorkspaceMemberAvatars({ - workspaceId, - isDryRun, - workspaceCustomFlatApplication, - fileRepository, - }: { - workspaceId: string; - isDryRun: boolean; - workspaceCustomFlatApplication: FlatApplication; - fileRepository: Repository; - }): Promise { - const { flatObjectMetadataMaps } = - await this.workspaceCacheService.getOrRecompute(workspaceId, [ - 'flatObjectMetadataMaps', - ]); - - const workspaceMemberObjectMetadata = - findFlatEntityByUniversalIdentifier({ - flatEntityMaps: flatObjectMetadataMaps, - universalIdentifier: - STANDARD_OBJECTS.workspaceMember.universalIdentifier, - }); - - if (!isDefined(workspaceMemberObjectMetadata)) { - this.logger.warn( - `Workspace member object metadata not found for workspace ${workspaceId}, skipping member avatar migration`, - ); - - return; - } - - const workspaceMemberRepository = - await this.twentyORMGlobalManager.getRepository( - workspaceId, - 'workspaceMember', - { shouldBypassPermissionChecks: true }, - ); - - const workspaceMembers = await workspaceMemberRepository.find({ - where: { - avatarUrl: And(Not(IsNull()), Not(Like(`%${FileFolder.CorePicture}%`))), - }, - select: ['id', 'avatarUrl'], - }); - - if (workspaceMembers.length === 0) { - this.logger.log( - `No workspace member avatars to migrate for workspace ${workspaceId}`, - ); - - return; - } - - this.logger.log( - `Found ${workspaceMembers.length} workspace member avatar(s) to migrate in workspace ${workspaceId}`, - ); - - for (const workspaceMember of workspaceMembers) { - if (!isNonEmptyString(workspaceMember.avatarUrl)) { - this.logger.warn( - `Skipping workspace member ${workspaceMember.id} - invalid avatarUrl`, - ); - - continue; - } - - try { - const { type: fileExtension } = extractFolderPathFilenameAndTypeOrThrow( - workspaceMember.avatarUrl, - ); - - const fileId = v4(); - const newFilename = `${fileId}${isNonEmptyString(fileExtension) ? `.${fileExtension}` : ''}`; - const newResourcePath = `${FileFolder.CorePicture}/${newFilename}`; - - if (!isDryRun) { - await this.fileStorageService.copyLegacy({ - from: { - folderPath: `workspace-${workspaceId}`, - filename: workspaceMember.avatarUrl, - }, - to: { - folderPath: `${workspaceId}/${workspaceCustomFlatApplication.universalIdentifier}`, - filename: newResourcePath, - }, - }); - - const fileEntity = fileRepository.create({ - id: fileId, - path: newResourcePath, - workspaceId, - applicationId: workspaceCustomFlatApplication.id, - size: -1, - settings: { - isTemporaryFile: false, - toDelete: false, - }, - }); - - await fileRepository.save(fileEntity); - - const signedUrl = this.fileUrlService.signFileByIdUrl({ - fileId, - workspaceId, - fileFolder: FileFolder.CorePicture, - }); - - await workspaceMemberRepository.update( - { id: workspaceMember.id }, - { - avatarUrl: signedUrl, - }, - ); - } - - this.logger.log( - `Migrated workspace member avatar ${workspaceMember.id}`, - ); - } catch (error) { - this.logger.error( - `Failed to migrate workspace member avatar ${workspaceMember.id} in workspace ${workspaceId}: ${error.message}`, - ); - throw error; - } - } - } -} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-18/1-18-upgrade-version-command.module.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-18/1-18-upgrade-version-command.module.ts deleted file mode 100644 index 3a812f931c..0000000000 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-18/1-18-upgrade-version-command.module.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { Module } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; - -import { BackfillFileSizeAndMimeTypeCommand } from 'src/database/commands/upgrade-version-command/1-18/1-18-backfill-file-size-and-mime-type.command'; -import { BackfillMessageChannelThrottleRetryAfterCommand } from 'src/database/commands/upgrade-version-command/1-18/1-18-backfill-message-channel-throttle-retry-after.command'; -import { BackfillStandardViewsAndFieldMetadataCommand } from 'src/database/commands/upgrade-version-command/1-18/1-18-backfill-standard-views-and-field-metadata.command'; -import { DeleteOrphanFavoritesCommand } from 'src/database/commands/upgrade-version-command/1-18/1-18-delete-orphan-favorites.command'; -import { MigrateActivityRichTextAttachmentFileIdsCommand } from 'src/database/commands/upgrade-version-command/1-18/1-18-migrate-activity-rich-text-attachment-file-ids.command'; -import { MigrateAttachmentFilesCommand } from 'src/database/commands/upgrade-version-command/1-18/1-18-migrate-attachment-files.command'; -import { MigrateFavoritesToNavigationMenuItemsCommand } from 'src/database/commands/upgrade-version-command/1-18/1-18-migrate-favorites-to-navigation-menu-items.command'; -import { MigratePersonAvatarFilesCommand } from 'src/database/commands/upgrade-version-command/1-18/1-18-migrate-person-avatar-files.command'; -import { MigrateWorkflowSendEmailAttachmentsCommand } from 'src/database/commands/upgrade-version-command/1-18/1-18-migrate-workflow-send-email-attachments.command'; -import { MigrateWorkspacePicturesCommand } from 'src/database/commands/upgrade-version-command/1-18/1-18-migrate-workspace-pictures.command'; -import { ApplicationModule } from 'src/engine/core-modules/application/application.module'; -import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; -import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; -import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity'; -import { FileModule } from 'src/engine/core-modules/file/file.module'; -import { SecureHttpClientModule } from 'src/engine/core-modules/secure-http-client/secure-http-client.module'; -import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user-workspace.module'; -import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; -import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; -import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { FieldMetadataModule } from 'src/engine/metadata-modules/field-metadata/field-metadata.module'; -import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module'; -import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module'; -import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module'; -import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module'; -import { AttachmentWorkspaceEntity } from 'src/modules/attachment/standard-objects/attachment.workspace-entity'; -import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity'; -import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; - -@Module({ - imports: [ - TypeOrmModule.forFeature([ - WorkspaceEntity, - FeatureFlagEntity, - PersonWorkspaceEntity, - FileEntity, - AttachmentWorkspaceEntity, - WorkspaceMemberWorkspaceEntity, - ObjectMetadataEntity, - FieldMetadataEntity, - ]), - DataSourceModule, - FeatureFlagModule, - WorkspaceCacheModule, - WorkspaceCacheStorageModule, - WorkspaceMetadataVersionModule, - FieldMetadataModule, - ApplicationModule, - FileModule, - UserWorkspaceModule, - WorkspaceMigrationModule, - SecureHttpClientModule, - ], - providers: [ - DeleteOrphanFavoritesCommand, - MigratePersonAvatarFilesCommand, - MigrateFavoritesToNavigationMenuItemsCommand, - MigrateAttachmentFilesCommand, - BackfillFileSizeAndMimeTypeCommand, - MigrateWorkspacePicturesCommand, - MigrateActivityRichTextAttachmentFileIdsCommand, - BackfillMessageChannelThrottleRetryAfterCommand, - BackfillStandardViewsAndFieldMetadataCommand, - MigrateWorkflowSendEmailAttachmentsCommand, - ], - exports: [ - DeleteOrphanFavoritesCommand, - MigratePersonAvatarFilesCommand, - MigrateFavoritesToNavigationMenuItemsCommand, - MigrateAttachmentFilesCommand, - MigrateActivityRichTextAttachmentFileIdsCommand, - BackfillMessageChannelThrottleRetryAfterCommand, - BackfillStandardViewsAndFieldMetadataCommand, - MigrateWorkspacePicturesCommand, - MigrateWorkflowSendEmailAttachmentsCommand, - BackfillFileSizeAndMimeTypeCommand, - ], -}) -export class V1_18_UpgradeVersionCommandModule {} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade-version-command.module.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade-version-command.module.ts index 18a057fee4..8cc3fcc2a3 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade-version-command.module.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade-version-command.module.ts @@ -1,8 +1,6 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { V1_17_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-17/1-17-upgrade-version-command.module'; -import { V1_18_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-18/1-18-upgrade-version-command.module'; import { V1_19_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-19/1-19-upgrade-version-command.module'; import { V1_20_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-20/1-20-upgrade-version-command.module'; import { UpgradeCommand } from 'src/database/commands/upgrade-version-command/upgrade.command'; @@ -15,8 +13,6 @@ import { WorkspaceVersionModule } from 'src/engine/workspace-manager/workspace-v @Module({ imports: [ TypeOrmModule.forFeature([WorkspaceEntity]), - V1_17_UpgradeVersionCommandModule, - V1_18_UpgradeVersionCommandModule, V1_19_UpgradeVersionCommandModule, V1_20_UpgradeVersionCommandModule, DataSourceModule, diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts index 70edda4b7e..acaa29b58e 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts @@ -10,24 +10,6 @@ import { type VersionCommands, } from 'src/database/commands/command-runners/upgrade.command-runner'; import { CoreMigrationRunnerService } from 'src/database/commands/core-migration-runner/services/core-migration-runner.service'; -import { BackfillApplicationPackageFilesCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-backfill-application-package-files.command'; -import { DeleteFileRecordsAndUpdateTableCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-delete-all-files-and-update-table.command'; -import { FixMorphRelationFieldNamesCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-fix-morph-relation-field-names.command'; -import { IdentifyWebhookMetadataCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-identify-webhook-metadata.command'; -import { MakeWebhookUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-make-webhook-universal-identifier-and-application-id-not-nullable-migration.command'; -import { MigrateAttachmentToMorphRelationsCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-migrate-attachment-to-morph-relations.command'; -import { MigrateNoteTargetToMorphRelationsCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-migrate-note-target-to-morph-relations.command'; -import { MigrateTaskTargetToMorphRelationsCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-migrate-task-target-to-morph-relations.command'; -import { BackfillFileSizeAndMimeTypeCommand } from 'src/database/commands/upgrade-version-command/1-18/1-18-backfill-file-size-and-mime-type.command'; -import { BackfillMessageChannelThrottleRetryAfterCommand } from 'src/database/commands/upgrade-version-command/1-18/1-18-backfill-message-channel-throttle-retry-after.command'; -import { BackfillStandardViewsAndFieldMetadataCommand } from 'src/database/commands/upgrade-version-command/1-18/1-18-backfill-standard-views-and-field-metadata.command'; -import { DeleteOrphanFavoritesCommand } from 'src/database/commands/upgrade-version-command/1-18/1-18-delete-orphan-favorites.command'; -import { MigrateActivityRichTextAttachmentFileIdsCommand } from 'src/database/commands/upgrade-version-command/1-18/1-18-migrate-activity-rich-text-attachment-file-ids.command'; -import { MigrateAttachmentFilesCommand } from 'src/database/commands/upgrade-version-command/1-18/1-18-migrate-attachment-files.command'; -import { MigrateFavoritesToNavigationMenuItemsCommand } from 'src/database/commands/upgrade-version-command/1-18/1-18-migrate-favorites-to-navigation-menu-items.command'; -import { MigratePersonAvatarFilesCommand } from 'src/database/commands/upgrade-version-command/1-18/1-18-migrate-person-avatar-files.command'; -import { MigrateWorkflowSendEmailAttachmentsCommand } from 'src/database/commands/upgrade-version-command/1-18/1-18-migrate-workflow-send-email-attachments.command'; -import { MigrateWorkspacePicturesCommand } from 'src/database/commands/upgrade-version-command/1-18/1-18-migrate-workspace-pictures.command'; import { AddMissingSystemFieldsToStandardObjectsCommand } from 'src/database/commands/upgrade-version-command/1-19/1-19-add-missing-system-fields-to-standard-objects.command'; import { BackfillMessageChannelMessageAssociationMessageFolderCommand } from 'src/database/commands/upgrade-version-command/1-19/1-19-backfill-message-channel-message-association-message-folder.command'; import { BackfillMissingStandardViewsCommand } from 'src/database/commands/upgrade-version-command/1-19/1-19-backfill-missing-standard-views.command'; @@ -74,28 +56,6 @@ export class UpgradeCommand extends UpgradeCommandRunner { protected readonly workspaceVersionService: WorkspaceVersionService, protected readonly coreMigrationRunnerService: CoreMigrationRunnerService, - // 1.17 Commands - protected readonly backfillApplicationPackageFilesCommand: BackfillApplicationPackageFilesCommand, - protected readonly deleteFileRecordsAndUpdateTableCommand: DeleteFileRecordsAndUpdateTableCommand, - protected readonly migrateAttachmentToMorphRelationsCommand: MigrateAttachmentToMorphRelationsCommand, - protected readonly migrateNoteTargetToMorphRelationsCommand: MigrateNoteTargetToMorphRelationsCommand, - protected readonly migrateTaskTargetToMorphRelationsCommand: MigrateTaskTargetToMorphRelationsCommand, - protected readonly identifyWebhookMetadataCommand: IdentifyWebhookMetadataCommand, - protected readonly makeWebhookUniversalIdentifierAndApplicationIdNotNullableMigrationCommand: MakeWebhookUniversalIdentifierAndApplicationIdNotNullableMigrationCommand, - protected readonly fixMorphRelationFieldNamesCommand: FixMorphRelationFieldNamesCommand, - - // 1.18 Commands - protected readonly deleteOrphanFavoritesCommand: DeleteOrphanFavoritesCommand, - protected readonly migrateFavoritesToNavigationMenuItemsCommand: MigrateFavoritesToNavigationMenuItemsCommand, - protected readonly migratePersonAvatarFilesCommand: MigratePersonAvatarFilesCommand, - protected readonly backfillFileSizeAndMimeTypeCommand: BackfillFileSizeAndMimeTypeCommand, - protected readonly migrateAttachmentFilesCommand: MigrateAttachmentFilesCommand, - protected readonly migrateActivityRichTextAttachmentFileIdsCommand: MigrateActivityRichTextAttachmentFileIdsCommand, - protected readonly backfillMessageChannelThrottleRetryAfterCommand: BackfillMessageChannelThrottleRetryAfterCommand, - protected readonly backfillStandardViewsAndFieldMetadataCommand: BackfillStandardViewsAndFieldMetadataCommand, - protected readonly migrateWorkspacePicturesCommand: MigrateWorkspacePicturesCommand, - protected readonly migrateWorkflowSendEmailAttachmentsCommand: MigrateWorkflowSendEmailAttachmentsCommand, - // 1.19 Commands protected readonly backfillSystemFieldsIsSystemCommand: BackfillSystemFieldsIsSystemCommand, protected readonly addMissingSystemFieldsToStandardObjectsCommand: AddMissingSystemFieldsToStandardObjectsCommand, @@ -132,34 +92,6 @@ export class UpgradeCommand extends UpgradeCommandRunner { coreMigrationRunnerService, ); - // Note: Required empty commands array to allow retrieving previous version - const commands_1160: VersionCommands = []; - - const commands_1170: VersionCommands = [ - this.migrateAttachmentToMorphRelationsCommand, - this.migrateNoteTargetToMorphRelationsCommand, - this.migrateTaskTargetToMorphRelationsCommand, - this.identifyWebhookMetadataCommand, - this - .makeWebhookUniversalIdentifierAndApplicationIdNotNullableMigrationCommand, - this.deleteFileRecordsAndUpdateTableCommand, - this.backfillApplicationPackageFilesCommand, - this.fixMorphRelationFieldNamesCommand, - ]; - - const commands_1180: VersionCommands = [ - this.deleteOrphanFavoritesCommand, - this.migrateFavoritesToNavigationMenuItemsCommand, - this.migratePersonAvatarFilesCommand, - this.migrateAttachmentFilesCommand, - this.migrateActivityRichTextAttachmentFileIdsCommand, - this.migrateWorkspacePicturesCommand, - this.migrateWorkflowSendEmailAttachmentsCommand, - this.backfillFileSizeAndMimeTypeCommand, - this.backfillMessageChannelThrottleRetryAfterCommand, - this.backfillStandardViewsAndFieldMetadataCommand, - ]; - const commands_1190: VersionCommands = [ this.fixRoleAndAgentUniversalIdentifiersCommand, this.backfillSystemFieldsIsSystemCommand, @@ -193,9 +125,6 @@ export class UpgradeCommand extends UpgradeCommandRunner { ]; this.allCommands = { - '1.16.0': commands_1160, - '1.17.0': commands_1170, - '1.18.0': commands_1180, '1.19.0': commands_1190, '1.20.0': commands_1200, }; diff --git a/packages/twenty-server/src/engine/constants/upgrade-command-supported-versions.constant.ts b/packages/twenty-server/src/engine/constants/upgrade-command-supported-versions.constant.ts index 8ba655f51d..b0072e504a 100644 --- a/packages/twenty-server/src/engine/constants/upgrade-command-supported-versions.constant.ts +++ b/packages/twenty-server/src/engine/constants/upgrade-command-supported-versions.constant.ts @@ -1,10 +1,4 @@ -export const UPGRADE_COMMAND_SUPPORTED_VERSIONS = [ - '1.16.0', - '1.17.0', - '1.18.0', - '1.19.0', - '1.20.0', -] as const; +export const UPGRADE_COMMAND_SUPPORTED_VERSIONS = ['1.19.0', '1.20.0'] as const; export type UpgradeCommandVersion = (typeof UPGRADE_COMMAND_SUPPORTED_VERSIONS)[number]; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/from-create-object-input-to-flat-object-metadata-and-flat-field-metadatas-to-create.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/from-create-object-input-to-flat-object-metadata-and-flat-field-metadatas-to-create.util.ts index 17e46d5080..9f2af66004 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/from-create-object-input-to-flat-object-metadata-and-flat-field-metadatas-to-create.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/from-create-object-input-to-flat-object-metadata-and-flat-field-metadatas-to-create.util.ts @@ -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; export const fromCreateObjectInputToFlatObjectMetadataAndFlatFieldMetadatasToCreate = ({ createObjectInput: rawCreateObjectInput, flatApplication, flatObjectMetadataMaps: existingFlatObjectMetadataMaps, - existingFeatureFlagsMap, }: FromCreateObjectInputToFlatObjectMetadataAndFlatFieldMetadatasToCreateArgs): { flatObjectMetadataToCreate: UniversalFlatObjectMetadata & { id: string }; relationTargetFlatFieldMetadataToCreate: UniversalFlatFieldMetadata[]; @@ -109,7 +105,6 @@ export const fromCreateObjectInputToFlatObjectMetadataAndFlatFieldMetadatasToCre existingFlatObjectMetadataMaps, sourceFlatObjectMetadata: universalFlatObjectMetadataToCreate, flatApplication, - existingFeatureFlagsMap, }); const objectFlatFieldMetadatas: UniversalFlatFieldMetadata[] = [ 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 ca658872c7..e1e3d8d5a0 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 @@ -406,7 +406,6 @@ export class ObjectMetadataService extends TypeOrmQueryService; -// 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)[]; - export type BuildDefaultRelationFieldsForCustomObjectArgs = { - existingFeatureFlagsMap: FeatureFlagMap; existingFlatObjectMetadataMaps: FlatEntityMaps; sourceFlatObjectMetadata: UniversalFlatObjectMetadata; flatApplication: FlatApplication; @@ -58,7 +50,6 @@ const EMPTY_SOURCE_AND_TARGET_FLAT_FIELD_METADATAS_RECORD: SourceAndTargetFlatFi }; export const buildDefaultRelationFlatFieldMetadatasForCustomObject = ({ - existingFeatureFlagsMap, existingFlatObjectMetadataMaps, sourceFlatObjectMetadata, flatApplication, @@ -79,21 +70,11 @@ export const buildDefaultRelationFlatFieldMetadatasForCustomObject = ({ const result = DEFAULT_RELATIONS_OBJECTS_STANDARD_IDS.reduce( (sourceAndTargetFlatFieldMetadatasRecord, objectMetadataNameSingular) => { - const isObjectMigratedFromOlderReleases = - 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 isObjectMigratedToMorphRelations = - isObjectMigratedFromOlderReleases || isFeatureFlagEnabled; + objectMetadataNameSingular === 'timelineActivity' || + objectMetadataNameSingular === 'attachment' || + objectMetadataNameSingular === 'noteTarget' || + objectMetadataNameSingular === 'taskTarget'; const targetFlatObjectMetadataId = objectIdByNameSingular[objectMetadataNameSingular]; diff --git a/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts b/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts index 14346db839..fd97be3de1 100644 --- a/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts +++ b/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts @@ -239,9 +239,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, diff --git a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-feature-flags.util.ts b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-feature-flags.util.ts index 00cd212959..f503d29d31 100644 --- a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-feature-flags.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-feature-flags.util.ts @@ -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, diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/constant/default-feature-flags.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/constant/default-feature-flags.ts index ca83586018..7eedcb7856 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/constant/default-feature-flags.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/constant/default-feature-flags.ts @@ -1,7 +1,3 @@ -import { FeatureFlagKey } from 'twenty-shared/types'; +import { type FeatureFlagKey } from 'twenty-shared/types'; -export const DEFAULT_FEATURE_FLAGS = [ - FeatureFlagKey.IS_ATTACHMENT_MIGRATED, - FeatureFlagKey.IS_NOTE_TARGET_MIGRATED, - FeatureFlagKey.IS_TASK_TARGET_MIGRATED, -] as const satisfies FeatureFlagKey[]; +export const DEFAULT_FEATURE_FLAGS = [] as const satisfies FeatureFlagKey[]; diff --git a/packages/twenty-shared/src/types/FeatureFlagKey.ts b/packages/twenty-shared/src/types/FeatureFlagKey.ts index d2af983392..d8003d157d 100644 --- a/packages/twenty-shared/src/types/FeatureFlagKey.ts +++ b/packages/twenty-shared/src/types/FeatureFlagKey.ts @@ -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',