chore: remove completed migration feature flags and upgrade commands <= 1.18 (#19074)
## 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
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
@@ -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<Attachment>({
|
||||
|
||||
@@ -4,12 +4,10 @@ import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getActivi
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { CoreObjectNameSingular } from 'twenty-shared/types';
|
||||
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { useApolloClient, useMutation } from '@apollo/client/react';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
FeatureFlagKey,
|
||||
FieldMetadataType,
|
||||
UploadFilesFieldFileDocument,
|
||||
} from '~/generated-metadata/graphql';
|
||||
@@ -19,10 +17,6 @@ export const useUploadAttachmentFile = () => {
|
||||
const [uploadFilesFieldFile] = useMutation(UploadFilesFieldFileDocument, {
|
||||
client: apolloClient,
|
||||
});
|
||||
const isAttachmentMigrated = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_ATTACHMENT_MIGRATED,
|
||||
);
|
||||
|
||||
const { objectMetadataItem: attachmentMetadata } = useObjectMetadataItem({
|
||||
objectNameSingular: CoreObjectNameSingular.Attachment,
|
||||
});
|
||||
@@ -58,7 +52,6 @@ export const useUploadAttachmentFile = () => {
|
||||
|
||||
const targetableObjectFieldIdName = getActivityTargetObjectFieldIdName({
|
||||
nameSingular: targetableObject.targetObjectNameSingular,
|
||||
isMorphRelation: isAttachmentMigrated,
|
||||
});
|
||||
|
||||
const attachmentToCreate = {
|
||||
|
||||
+2
-10
@@ -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),
|
||||
},
|
||||
}),
|
||||
},
|
||||
|
||||
+2
-19
@@ -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<EnrichedObjectMetadataItem[]>(
|
||||
objectMetadataItemsSelector,
|
||||
);
|
||||
const isNoteTargetMigrated = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_NOTE_TARGET_MIGRATED,
|
||||
);
|
||||
const isTaskTargetMigrated = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_TASK_TARGET_MIGRATED,
|
||||
);
|
||||
const isMorphRelation =
|
||||
objectNameSingular === CoreObjectNameSingular.Task
|
||||
? isTaskTargetMigrated
|
||||
: isNoteTargetMigrated;
|
||||
|
||||
const activityTargetsFilter = getActivityTargetsFilter({
|
||||
targetableObjects: targetableObjects,
|
||||
activityObjectNameSingular: objectNameSingular,
|
||||
objectMetadataItems,
|
||||
isMorphRelation,
|
||||
targetableObjects,
|
||||
});
|
||||
|
||||
const FIND_ACTIVITY_TARGETS_OPERATION_SIGNATURE =
|
||||
|
||||
+1
-13
@@ -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({
|
||||
|
||||
-15
@@ -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,
|
||||
],
|
||||
);
|
||||
|
||||
-3
@@ -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];
|
||||
|
||||
+7
-13
@@ -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$/, '');
|
||||
};
|
||||
|
||||
+1
-7
@@ -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`;
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
-7
@@ -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<Attachment>({
|
||||
|
||||
-6
@@ -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
|
||||
|
||||
-299
@@ -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<string, string[]>
|
||||
> = {
|
||||
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,
|
||||
],
|
||||
},
|
||||
};
|
||||
-212
@@ -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<WorkspaceEntity>,
|
||||
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<void> {
|
||||
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<void> {
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
-142
@@ -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<WorkspaceEntity>,
|
||||
@InjectRepository(FileEntity)
|
||||
private readonly fileRepository: Repository<FileEntity>,
|
||||
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
|
||||
protected readonly dataSourceService: DataSourceService,
|
||||
@InjectDataSource()
|
||||
private readonly coreDataSource: DataSource,
|
||||
) {
|
||||
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
|
||||
}
|
||||
|
||||
override async runOnWorkspace(args: RunOnWorkspaceArgs): Promise<void> {
|
||||
if (this.hasRunOnce) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.deleteFileRecords(args);
|
||||
await this.updateFileTable();
|
||||
await this.addFileEntityUniqueConstraint(args);
|
||||
this.hasRunOnce = true;
|
||||
}
|
||||
|
||||
private async updateFileTable(): Promise<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
-337
@@ -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<WorkspaceEntity>,
|
||||
@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<void> {
|
||||
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<MismatchedField[]> {
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
-117
@@ -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<WorkspaceEntity>,
|
||||
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<void> {
|
||||
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<void> {
|
||||
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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
-71
@@ -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<WorkspaceEntity>,
|
||||
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
|
||||
protected readonly dataSourceService: DataSourceService,
|
||||
@InjectDataSource()
|
||||
private readonly coreDataSource: DataSource,
|
||||
) {
|
||||
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
-334
@@ -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<FieldMetadataType.RELATION>;
|
||||
|
||||
@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<WorkspaceEntity>,
|
||||
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<void> {
|
||||
const isMigrated = await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IS_ATTACHMENT_MIGRATED,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
this.logger.log(`Migrating attachments for workspace ${workspaceId}`);
|
||||
|
||||
if (isMigrated) {
|
||||
this.logger.log(`Attachment migration already completed. Skipping...`);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.dryRun) {
|
||||
this.logger.log(
|
||||
`Would have migrated attachments for workspace ${workspaceId}. Skipping...`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const queryRunner = this.coreDataSource.createQueryRunner();
|
||||
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
try {
|
||||
const schemaName = getWorkspaceSchemaName(workspaceId);
|
||||
const tableName = 'attachment';
|
||||
|
||||
const {
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
flatApplicationMaps,
|
||||
} = await this.workspaceCacheService.getOrRecompute(workspaceId, [
|
||||
'flatObjectMetadataMaps',
|
||||
'flatFieldMetadataMaps',
|
||||
'flatApplicationMaps',
|
||||
]);
|
||||
|
||||
const attachmentTargetFieldUniversalIdentifiers = new Set<string>([
|
||||
STANDARD_OBJECTS.attachment.fields.targetTask.universalIdentifier,
|
||||
STANDARD_OBJECTS.attachment.fields.targetNote.universalIdentifier,
|
||||
STANDARD_OBJECTS.attachment.fields.targetPerson.universalIdentifier,
|
||||
STANDARD_OBJECTS.attachment.fields.targetCompany.universalIdentifier,
|
||||
STANDARD_OBJECTS.attachment.fields.targetOpportunity
|
||||
.universalIdentifier,
|
||||
STANDARD_OBJECTS.attachment.fields.targetDashboard.universalIdentifier,
|
||||
STANDARD_OBJECTS.attachment.fields.targetWorkflow.universalIdentifier,
|
||||
]);
|
||||
|
||||
const attachmentObjectMetadata =
|
||||
findFlatEntityByUniversalIdentifier<FlatObjectMetadata>({
|
||||
flatEntityMaps: flatObjectMetadataMaps,
|
||||
universalIdentifier: STANDARD_OBJECTS.attachment.universalIdentifier,
|
||||
});
|
||||
|
||||
if (!attachmentObjectMetadata) {
|
||||
this.logger.error(
|
||||
`🟥 Attachment object metadata not found for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const attachmentFieldMetadatas = getFlatFieldsFromFlatObjectMetadata(
|
||||
attachmentObjectMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
);
|
||||
|
||||
const attachmentTargetRelationFields = attachmentFieldMetadatas
|
||||
.filter(isMorphOrRelationFlatFieldMetadata)
|
||||
.filter(
|
||||
(field): field is FlatFieldMetadata<FieldMetadataType.RELATION> =>
|
||||
field.type === FieldMetadataType.RELATION,
|
||||
)
|
||||
.filter((field) => {
|
||||
const isStandardAppField = this.isTwentyStandardApplicationField({
|
||||
field,
|
||||
flatApplicationMaps,
|
||||
workspaceId,
|
||||
});
|
||||
const isStandardTarget =
|
||||
isStandardAppField &&
|
||||
attachmentTargetFieldUniversalIdentifiers.has(
|
||||
field.universalIdentifier,
|
||||
);
|
||||
const targetObjectMetadata =
|
||||
findFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityId: field.relationTargetObjectMetadataId,
|
||||
flatEntityMaps: flatObjectMetadataMaps,
|
||||
});
|
||||
const isCustomTarget =
|
||||
!isStandardAppField && targetObjectMetadata?.isCustom === true;
|
||||
|
||||
return isStandardTarget || isCustomTarget;
|
||||
});
|
||||
|
||||
const fieldMigrations = attachmentTargetRelationFields.map((field) => {
|
||||
const newFieldName = `target${capitalize(field.name)}`;
|
||||
const relationSettings: RelationFieldMetadataSettings = field.settings;
|
||||
const oldJoinColumnName =
|
||||
relationSettings?.joinColumnName ??
|
||||
computeMorphOrRelationFieldJoinColumnName({ name: field.name });
|
||||
const newJoinColumnName = computeMorphOrRelationFieldJoinColumnName({
|
||||
name: newFieldName,
|
||||
});
|
||||
|
||||
return {
|
||||
field,
|
||||
newFieldName,
|
||||
oldJoinColumnName,
|
||||
newJoinColumnName,
|
||||
};
|
||||
});
|
||||
|
||||
for (const { oldJoinColumnName, newJoinColumnName } of fieldMigrations) {
|
||||
if (oldJoinColumnName === newJoinColumnName) {
|
||||
this.logger.log(
|
||||
`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
|
||||
);
|
||||
}
|
||||
}
|
||||
-157
@@ -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<WorkspaceEntity>,
|
||||
protected readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
|
||||
protected readonly dataSourceService: DataSourceService,
|
||||
@InjectRepository(ViewFilterEntity)
|
||||
private readonly viewFilterRepository: Repository<ViewFilterEntity>,
|
||||
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<void> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
-331
@@ -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<FieldMetadataType.RELATION>;
|
||||
|
||||
@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<WorkspaceEntity>,
|
||||
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<void> {
|
||||
const isMigrated = await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IS_NOTE_TARGET_MIGRATED,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
this.logger.log(`Migrating noteTarget for workspace ${workspaceId}`);
|
||||
|
||||
if (isMigrated) {
|
||||
this.logger.log(`NoteTarget migration already completed. Skipping...`);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.dryRun) {
|
||||
this.logger.log(
|
||||
`Would have migrated noteTarget for workspace ${workspaceId}. Skipping...`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const queryRunner = this.coreDataSource.createQueryRunner();
|
||||
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
try {
|
||||
const schemaName = getWorkspaceSchemaName(workspaceId);
|
||||
const tableName = 'noteTarget';
|
||||
|
||||
const {
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
flatApplicationMaps,
|
||||
} = await this.workspaceCacheService.getOrRecompute(workspaceId, [
|
||||
'flatObjectMetadataMaps',
|
||||
'flatFieldMetadataMaps',
|
||||
'flatApplicationMaps',
|
||||
]);
|
||||
|
||||
const noteTargetFieldUniversalIdentifiers = new Set<string>([
|
||||
STANDARD_OBJECTS.noteTarget.fields.targetPerson.universalIdentifier,
|
||||
STANDARD_OBJECTS.noteTarget.fields.targetCompany.universalIdentifier,
|
||||
STANDARD_OBJECTS.noteTarget.fields.targetOpportunity
|
||||
.universalIdentifier,
|
||||
]);
|
||||
|
||||
const noteTargetObjectMetadata =
|
||||
findFlatEntityByUniversalIdentifier<FlatObjectMetadata>({
|
||||
flatEntityMaps: flatObjectMetadataMaps,
|
||||
universalIdentifier: STANDARD_OBJECTS.noteTarget.universalIdentifier,
|
||||
});
|
||||
|
||||
if (!noteTargetObjectMetadata) {
|
||||
this.logger.error(
|
||||
`🟥 ${tableName} object metadata not found for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const noteTargetFieldMetadatas = getFlatFieldsFromFlatObjectMetadata(
|
||||
noteTargetObjectMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
);
|
||||
|
||||
const noteTargetRelationFields = noteTargetFieldMetadatas
|
||||
.filter(isMorphOrRelationFlatFieldMetadata)
|
||||
.filter((field) => field.type === FieldMetadataType.RELATION)
|
||||
.filter((field) => {
|
||||
const isStandardAppField = this.isTwentyStandardApplicationField({
|
||||
field,
|
||||
flatApplicationMaps,
|
||||
workspaceId,
|
||||
});
|
||||
const isStandardTarget =
|
||||
isStandardAppField &&
|
||||
noteTargetFieldUniversalIdentifiers.has(field.universalIdentifier);
|
||||
const targetObjectMetadata = field.relationTargetObjectMetadataId
|
||||
? findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityMaps: flatObjectMetadataMaps,
|
||||
flatEntityId: field.relationTargetObjectMetadataId,
|
||||
})
|
||||
: undefined;
|
||||
const isCustomTarget =
|
||||
!isStandardAppField && targetObjectMetadata?.isCustom === true;
|
||||
|
||||
return isStandardTarget || isCustomTarget;
|
||||
});
|
||||
|
||||
const fieldMigrations = noteTargetRelationFields.map((field) => {
|
||||
const newFieldName = `target${capitalize(field.name)}`;
|
||||
const newFieldLabel = 'Target';
|
||||
const relationSettings: RelationFieldMetadataSettings = field.settings;
|
||||
const oldJoinColumnName =
|
||||
relationSettings?.joinColumnName ??
|
||||
computeMorphOrRelationFieldJoinColumnName({ name: field.name });
|
||||
const newJoinColumnName = computeMorphOrRelationFieldJoinColumnName({
|
||||
name: newFieldName,
|
||||
});
|
||||
|
||||
return {
|
||||
field,
|
||||
newFieldName,
|
||||
newFieldLabel,
|
||||
oldJoinColumnName,
|
||||
newJoinColumnName,
|
||||
};
|
||||
});
|
||||
|
||||
// Rename columns
|
||||
for (const { oldJoinColumnName, newJoinColumnName } of fieldMigrations) {
|
||||
if (oldJoinColumnName === newJoinColumnName) {
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
-85
@@ -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<WorkspaceEntity>,
|
||||
protected readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
|
||||
protected readonly dataSourceService: DataSourceService,
|
||||
) {
|
||||
super(workspaceRepository, globalWorkspaceOrmManager, dataSourceService);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
workspaceId,
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
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}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
-331
@@ -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<FieldMetadataType.RELATION>;
|
||||
|
||||
@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<WorkspaceEntity>,
|
||||
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<void> {
|
||||
const isMigrated = await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IS_TASK_TARGET_MIGRATED,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
this.logger.log(`Migrating taskTarget for workspace ${workspaceId}`);
|
||||
|
||||
if (isMigrated) {
|
||||
this.logger.log(`TaskTarget migration already completed. Skipping...`);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.dryRun) {
|
||||
this.logger.log(
|
||||
`Would have migrated taskTarget for workspace ${workspaceId}. Skipping...`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const queryRunner = this.coreDataSource.createQueryRunner();
|
||||
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
try {
|
||||
const schemaName = getWorkspaceSchemaName(workspaceId);
|
||||
const tableName = 'taskTarget';
|
||||
|
||||
const {
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
flatApplicationMaps,
|
||||
} = await this.workspaceCacheService.getOrRecompute(workspaceId, [
|
||||
'flatObjectMetadataMaps',
|
||||
'flatFieldMetadataMaps',
|
||||
'flatApplicationMaps',
|
||||
]);
|
||||
|
||||
const taskTargetFieldUniversalIdentifiers = new Set<string>([
|
||||
STANDARD_OBJECTS.taskTarget.fields.targetPerson.universalIdentifier,
|
||||
STANDARD_OBJECTS.taskTarget.fields.targetCompany.universalIdentifier,
|
||||
STANDARD_OBJECTS.taskTarget.fields.targetOpportunity
|
||||
.universalIdentifier,
|
||||
]);
|
||||
|
||||
const taskTargetObjectMetadata =
|
||||
findFlatEntityByUniversalIdentifier<FlatObjectMetadata>({
|
||||
flatEntityMaps: flatObjectMetadataMaps,
|
||||
universalIdentifier: STANDARD_OBJECTS.taskTarget.universalIdentifier,
|
||||
});
|
||||
|
||||
if (!taskTargetObjectMetadata) {
|
||||
this.logger.error(
|
||||
`🟥 ${tableName} object metadata not found for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const taskTargetFieldMetadatas = getFlatFieldsFromFlatObjectMetadata(
|
||||
taskTargetObjectMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
);
|
||||
|
||||
const taskTargetRelationFields = taskTargetFieldMetadatas
|
||||
.filter(isMorphOrRelationFlatFieldMetadata)
|
||||
.filter((field) => field.type === FieldMetadataType.RELATION)
|
||||
.filter((field) => {
|
||||
const isStandardAppField = this.isTwentyStandardApplicationField({
|
||||
field,
|
||||
flatApplicationMaps,
|
||||
workspaceId,
|
||||
});
|
||||
const isStandardTarget =
|
||||
isStandardAppField &&
|
||||
taskTargetFieldUniversalIdentifiers.has(field.universalIdentifier);
|
||||
const targetObjectMetadata = field.relationTargetObjectMetadataId
|
||||
? findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityMaps: flatObjectMetadataMaps,
|
||||
flatEntityId: field.relationTargetObjectMetadataId,
|
||||
})
|
||||
: undefined;
|
||||
const isCustomTarget =
|
||||
!isStandardAppField && targetObjectMetadata?.isCustom === true;
|
||||
|
||||
return isStandardTarget || isCustomTarget;
|
||||
});
|
||||
|
||||
const fieldMigrations = taskTargetRelationFields.map((field) => {
|
||||
const newFieldName = `target${capitalize(field.name)}`;
|
||||
const newFieldLabel = 'Target';
|
||||
const relationSettings: RelationFieldMetadataSettings = field.settings;
|
||||
const oldJoinColumnName =
|
||||
relationSettings?.joinColumnName ??
|
||||
computeMorphOrRelationFieldJoinColumnName({ name: field.name });
|
||||
const newJoinColumnName = computeMorphOrRelationFieldJoinColumnName({
|
||||
name: newFieldName,
|
||||
});
|
||||
|
||||
return {
|
||||
field,
|
||||
newFieldName,
|
||||
newFieldLabel,
|
||||
oldJoinColumnName,
|
||||
newJoinColumnName,
|
||||
};
|
||||
});
|
||||
|
||||
// Rename columns
|
||||
for (const { oldJoinColumnName, newJoinColumnName } of fieldMigrations) {
|
||||
if (oldJoinColumnName === newJoinColumnName) {
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
-93
@@ -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 {}
|
||||
-98
@@ -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);
|
||||
});
|
||||
});
|
||||
-95
@@ -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 };
|
||||
};
|
||||
-184
@@ -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<WorkspaceEntity>,
|
||||
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<void> {
|
||||
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}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
-120
@@ -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<WorkspaceEntity>,
|
||||
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<void> {
|
||||
this.logger.log(
|
||||
`Backfilling throttleRetryAfter field for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
const { flatObjectMetadataMaps } =
|
||||
await this.workspaceCacheService.getOrRecompute(workspaceId, [
|
||||
'flatObjectMetadataMaps',
|
||||
]);
|
||||
|
||||
const messageChannelObjectMetadata =
|
||||
findFlatEntityByUniversalIdentifier<FlatObjectMetadata>({
|
||||
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}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
-753
@@ -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<WorkspaceEntity>,
|
||||
@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<void> {
|
||||
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<void> {
|
||||
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}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
-87
@@ -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<WorkspaceEntity>,
|
||||
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
|
||||
protected readonly dataSourceService: DataSourceService,
|
||||
private readonly workspaceCacheService: WorkspaceCacheService,
|
||||
) {
|
||||
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
workspaceId,
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
this.logger.log(`Deleting orphan favorites for workspace ${workspaceId}`);
|
||||
|
||||
const { flatViewMaps } = await this.workspaceCacheService.getOrRecompute(
|
||||
workspaceId,
|
||||
['flatViewMaps'],
|
||||
);
|
||||
|
||||
const favoriteRepository =
|
||||
await this.twentyORMGlobalManager.getRepository<FavoriteWorkspaceEntity>(
|
||||
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}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
-468
@@ -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<string, unknown>;
|
||||
|
||||
@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<WorkspaceEntity>,
|
||||
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<void> {
|
||||
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<FlatObjectMetadata>({
|
||||
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<AttachmentWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'attachment',
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
|
||||
const noteRepository =
|
||||
await this.twentyORMGlobalManager.getRepository<NoteWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'note',
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
|
||||
const taskRepository =
|
||||
await this.twentyORMGlobalManager.getRepository<TaskWorkspaceEntity>(
|
||||
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<NoteWorkspaceEntity>
|
||||
| WorkspaceRepository<TaskWorkspaceEntity>;
|
||||
attachmentRepository: Awaited<
|
||||
ReturnType<GlobalWorkspaceOrmManager['getRepository']>
|
||||
>;
|
||||
fileRepository: Repository<FileEntity>;
|
||||
activityType: 'note' | 'task';
|
||||
targetColumnName: 'targetNoteId' | 'targetTaskId';
|
||||
workspaceId: string;
|
||||
twentyStandardApplication: { id: string; universalIdentifier: string };
|
||||
fileFieldMetadata: { universalIdentifier: string };
|
||||
isDryRun: boolean;
|
||||
}): Promise<void> {
|
||||
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<string, unknown>) || {};
|
||||
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<string, unknown>) || {};
|
||||
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<GlobalWorkspaceOrmManager['getRepository']>
|
||||
>,
|
||||
targetColumnName: 'targetNoteId' | 'targetTaskId',
|
||||
activityId: string,
|
||||
): Promise<Map<string, string>> {
|
||||
const urlToFileIdMap = new Map<string, string>();
|
||||
|
||||
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, string>,
|
||||
): 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<GlobalWorkspaceOrmManager['getRepository']>
|
||||
>,
|
||||
fileRepository: Repository<FileEntity>,
|
||||
targetColumnName: 'targetNoteId' | 'targetTaskId',
|
||||
activityId: string,
|
||||
workspaceId: string,
|
||||
twentyStandardApplication: { id: string; universalIdentifier: string },
|
||||
fileFieldMetadata: { universalIdentifier: string },
|
||||
): Promise<string | undefined> {
|
||||
try {
|
||||
const props = block.props as Record<string, unknown>;
|
||||
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}`;
|
||||
};
|
||||
}
|
||||
-297
@@ -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<WorkspaceEntity>,
|
||||
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<void> {
|
||||
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<FlatObjectMetadata>({
|
||||
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<AttachmentWorkspaceEntity>(
|
||||
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);
|
||||
};
|
||||
}
|
||||
-782
@@ -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<WorkspaceEntity>,
|
||||
@InjectRepository(ObjectMetadataEntity)
|
||||
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
|
||||
@InjectRepository(FieldMetadataEntity)
|
||||
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
|
||||
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<void> {
|
||||
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<string, Map<string, string>>;
|
||||
flatNavigationMenuItemsToCreate: FlatNavigationMenuItem[];
|
||||
}> {
|
||||
const existingFlatNavigationMenuItemIds = Object.keys(
|
||||
existingFlatNavigationMenuItemMaps.universalIdentifierById,
|
||||
);
|
||||
|
||||
const favoriteFolderRepository =
|
||||
await this.twentyORMGlobalManager.getRepository<FavoriteFolderWorkspaceEntity>(
|
||||
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<string, Map<string, string>>(); // 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<FlatObjectMetadata>;
|
||||
flatViewMaps: FlatEntityMaps<FlatView>;
|
||||
folderIdMapping: Map<string, Map<string, string>>;
|
||||
existingFlatNavigationMenuItemMaps: FlatNavigationMenuItemMaps;
|
||||
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
|
||||
}): Promise<{
|
||||
migratedFavoriteIds: string[];
|
||||
flatNavigationMenuItemsToCreate: FlatNavigationMenuItem[];
|
||||
}> {
|
||||
const favoriteRepository =
|
||||
await this.twentyORMGlobalManager.getRepository<FavoriteWorkspaceEntity>(
|
||||
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<FieldMetadataType.RELATION> =>
|
||||
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<FlatView>;
|
||||
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<string | null> {
|
||||
if (!isDefined(workspaceMemberId)) {
|
||||
return null;
|
||||
}
|
||||
const authContext = buildSystemAuthContext(workspaceId);
|
||||
|
||||
try {
|
||||
const workspaceMember =
|
||||
await this.twentyORMGlobalManager.executeInWorkspaceContext(
|
||||
async () => {
|
||||
const workspaceMemberRepository =
|
||||
await this.twentyORMGlobalManager.getRepository<WorkspaceMemberWorkspaceEntity>(
|
||||
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<FlatObjectMetadata>,
|
||||
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<string, string | undefined>
|
||||
)[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<void> {
|
||||
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)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
-299
@@ -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<WorkspaceEntity>,
|
||||
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<void> {
|
||||
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<FlatObjectMetadata>({
|
||||
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<PersonWorkspaceEntity>(
|
||||
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);
|
||||
};
|
||||
}
|
||||
-211
@@ -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<WorkspaceEntity>,
|
||||
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<void> {
|
||||
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<WorkflowVersionWorkspaceEntity>(
|
||||
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}`,
|
||||
);
|
||||
};
|
||||
}
|
||||
-381
@@ -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<WorkspaceEntity>,
|
||||
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<void> {
|
||||
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<FileEntity>;
|
||||
}): Promise<void> {
|
||||
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<FileEntity>;
|
||||
}): Promise<void> {
|
||||
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<void> {
|
||||
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<FileEntity>;
|
||||
}): Promise<void> {
|
||||
const { flatObjectMetadataMaps } =
|
||||
await this.workspaceCacheService.getOrRecompute(workspaceId, [
|
||||
'flatObjectMetadataMaps',
|
||||
]);
|
||||
|
||||
const workspaceMemberObjectMetadata =
|
||||
findFlatEntityByUniversalIdentifier<FlatObjectMetadata>({
|
||||
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<WorkspaceMemberWorkspaceEntity>(
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-83
@@ -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 {}
|
||||
-4
@@ -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,
|
||||
|
||||
-71
@@ -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,
|
||||
};
|
||||
|
||||
+1
-7
@@ -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];
|
||||
|
||||
-5
@@ -4,8 +4,6 @@ import {
|
||||
} from 'twenty-shared/utils';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { type FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface';
|
||||
|
||||
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
|
||||
import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
|
||||
import { type CreateObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/create-object.input';
|
||||
@@ -20,14 +18,12 @@ type FromCreateObjectInputToFlatObjectMetadataAndFlatFieldMetadatasToCreateArgs
|
||||
{
|
||||
createObjectInput: CreateObjectInput;
|
||||
flatApplication: FlatApplication;
|
||||
existingFeatureFlagsMap: FeatureFlagMap;
|
||||
} & Pick<AllFlatEntityMaps, 'flatObjectMetadataMaps'>;
|
||||
export const fromCreateObjectInputToFlatObjectMetadataAndFlatFieldMetadatasToCreate =
|
||||
({
|
||||
createObjectInput: rawCreateObjectInput,
|
||||
flatApplication,
|
||||
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
|
||||
existingFeatureFlagsMap,
|
||||
}: FromCreateObjectInputToFlatObjectMetadataAndFlatFieldMetadatasToCreateArgs): {
|
||||
flatObjectMetadataToCreate: UniversalFlatObjectMetadata & { id: string };
|
||||
relationTargetFlatFieldMetadataToCreate: UniversalFlatFieldMetadata[];
|
||||
@@ -109,7 +105,6 @@ export const fromCreateObjectInputToFlatObjectMetadataAndFlatFieldMetadatasToCre
|
||||
existingFlatObjectMetadataMaps,
|
||||
sourceFlatObjectMetadata: universalFlatObjectMetadataToCreate,
|
||||
flatApplication,
|
||||
existingFeatureFlagsMap,
|
||||
});
|
||||
|
||||
const objectFlatFieldMetadatas: UniversalFlatFieldMetadata[] = [
|
||||
|
||||
-1
@@ -406,7 +406,6 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
createObjectInput,
|
||||
flatApplication: resolvedOwnerFlatApplication,
|
||||
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
|
||||
existingFeatureFlagsMap,
|
||||
});
|
||||
|
||||
const flatDefaultViewToCreate = this.computeFlatViewToCreate({
|
||||
|
||||
+5
-24
@@ -2,10 +2,9 @@ import {
|
||||
STANDARD_OBJECTS,
|
||||
DEFAULT_RELATIONS_OBJECTS_STANDARD_IDS,
|
||||
} from 'twenty-shared/metadata';
|
||||
import { FieldMetadataType, FeatureFlagKey } from 'twenty-shared/types';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { capitalize, isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface';
|
||||
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
|
||||
|
||||
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
|
||||
@@ -34,14 +33,7 @@ const morphIdByRelationObjectNameSingular = {
|
||||
string | null
|
||||
>;
|
||||
|
||||
// TODO: once we have finished migrating, we can delete custom code
|
||||
// once we migrate timeline activity to morph relations, we can add it.
|
||||
// another way to check if an object is migrated to morph relations is to check if the feature flag is enabled
|
||||
const DEFAULT_MORPH_RELATIONS_OBJECTS_STANDARD_IDS =
|
||||
[] as const satisfies (keyof typeof STANDARD_OBJECTS)[];
|
||||
|
||||
export type BuildDefaultRelationFieldsForCustomObjectArgs = {
|
||||
existingFeatureFlagsMap: FeatureFlagMap;
|
||||
existingFlatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>;
|
||||
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>(
|
||||
(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];
|
||||
|
||||
-3
@@ -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,
|
||||
|
||||
-15
@@ -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,
|
||||
|
||||
+2
-6
@@ -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[];
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user