TimelineActivity migration to morph (#15652)
# TimelineActivity migration to morph - Creates `timelineActivities2` relations on Company, Dashboard, Note, Opportunity, Person, Task, Workflow, WorkflowRun, and WorkflowVersion entities with proper metadata and cascade delete behavior. It was required to create standard fields as well since the mapObjectMetadataByUniqueIdentifier needs it. otherwise the fields won't be considered - Feature Flag `IS_TIMELINE_ACTIVITY_MIGRATED` necessary to have the two states in parallel. It is used as a stamp once the migration has been run - Migration is done using the coreDataSource. Why ? even though is unsafe to use, the first implementation of the migration took forever on each workspace. See [this commit](https://github.com/twentyhq/twenty/pull/15652/commits/477011e8d7d4c580f79ba7ec4a8fb002a3ec86b2) The plan for this complex migration is as follows :  Note: we will need to rename fields in the release 1.12 (there is no easy way to do all this in one release)
This commit is contained in:
+8
-7
@@ -324,13 +324,13 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
workspaceId,
|
||||
},
|
||||
);
|
||||
const { flatObjectMetadataMaps: existingFlatObjectMetadataMaps } =
|
||||
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatObjectMetadataMaps'],
|
||||
},
|
||||
);
|
||||
const {
|
||||
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
|
||||
featureFlagsMap: existingFeatureFlagsMap,
|
||||
} = await this.workspaceCacheService.getOrRecompute(workspaceId, [
|
||||
'flatObjectMetadataMaps',
|
||||
'featureFlagsMap',
|
||||
]);
|
||||
|
||||
const {
|
||||
flatObjectMetadataToCreate,
|
||||
@@ -343,6 +343,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
workspaceCustomApplicationId:
|
||||
applicationId ?? workspaceCustomFlatApplication.id,
|
||||
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
|
||||
existingFeatureFlagsMap,
|
||||
});
|
||||
|
||||
const optimisticFlatFieldMetadataMaps = [
|
||||
|
||||
+127
-194
@@ -1,169 +1,26 @@
|
||||
import { type STANDARD_OBJECT_IDS } from 'twenty-shared/metadata';
|
||||
import { FieldMetadataType, RelationOnDeleteAction } from 'twenty-shared/types';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { capitalize, isDefined } from 'twenty-shared/utils';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
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 MorphOrRelationFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/types/morph-or-relation-field-metadata-type.type';
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util';
|
||||
import { type 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 { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { generateMorphOrRelationFlatFieldMetadataPair } from 'src/engine/metadata-modules/flat-field-metadata/utils/generate-morph-or-relation-flat-field-metadata-pair.util';
|
||||
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
import {
|
||||
ObjectMetadataException,
|
||||
ObjectMetadataExceptionCode,
|
||||
} from 'src/engine/metadata-modules/object-metadata/object-metadata.exception';
|
||||
import { buildDescriptionForRelationFieldMetadataOnFromField } from 'src/engine/metadata-modules/object-metadata/utils/build-description-for-relation-field-on-from-field.util';
|
||||
import { buildDescriptionForRelationFieldMetadataOnToField } from 'src/engine/metadata-modules/object-metadata/utils/build-description-for-relation-field-on-to-field.util';
|
||||
import {
|
||||
CUSTOM_OBJECT_STANDARD_FIELD_IDS,
|
||||
STANDARD_OBJECT_FIELD_IDS,
|
||||
TIMELINE_ACTIVITY_STANDARD_FIELD_IDS,
|
||||
} from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
|
||||
import { STANDARD_OBJECT_ICONS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-icons';
|
||||
import { createRelationDeterministicUuid } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/create-deterministic-uuid.util';
|
||||
|
||||
const generateSourceFlatFieldMetadata = ({
|
||||
workspaceId,
|
||||
targetFlatObjectMetadata,
|
||||
sourceFlatObjectMetadata,
|
||||
}: Omit<
|
||||
BuildDefaultRelationFieldsForCustomObjectArgs,
|
||||
'existingFlatObjectMetadataMaps'
|
||||
> & {
|
||||
sourceFlatObjectMetadata: FlatObjectMetadata;
|
||||
targetFlatObjectMetadata: FlatObjectMetadata;
|
||||
}): FlatFieldMetadata<FieldMetadataType.RELATION> => {
|
||||
const { description } = buildDescriptionForRelationFieldMetadataOnFromField({
|
||||
relationObjectMetadataNamePlural: targetFlatObjectMetadata.namePlural,
|
||||
targetObjectLabelSingular: sourceFlatObjectMetadata.labelSingular,
|
||||
});
|
||||
|
||||
const standardId =
|
||||
CUSTOM_OBJECT_STANDARD_FIELD_IDS[
|
||||
targetFlatObjectMetadata.namePlural as keyof typeof CUSTOM_OBJECT_STANDARD_FIELD_IDS
|
||||
];
|
||||
|
||||
if (!isDefined(standardId)) {
|
||||
throw new ObjectMetadataException(
|
||||
`Standard field ID not found for target object ${targetFlatObjectMetadata.namePlural}`,
|
||||
ObjectMetadataExceptionCode.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
|
||||
const createdAt = new Date().toISOString();
|
||||
const sourceFieldMetadataId = v4();
|
||||
const targetFieldMetadataId = v4();
|
||||
const icon =
|
||||
STANDARD_OBJECT_ICONS[
|
||||
targetFlatObjectMetadata.nameSingular as keyof typeof STANDARD_OBJECT_ICONS
|
||||
] || 'IconBuildingSkyscraper';
|
||||
|
||||
return {
|
||||
calendarViewIds: [],
|
||||
mainGroupByFieldMetadataViewIds: [],
|
||||
kanbanAggregateOperationViewIds: [],
|
||||
viewFilterIds: [],
|
||||
viewFieldIds: [],
|
||||
createdAt,
|
||||
updatedAt: createdAt,
|
||||
defaultValue: null,
|
||||
description: description,
|
||||
icon,
|
||||
id: sourceFieldMetadataId,
|
||||
isActive: true,
|
||||
isCustom: false,
|
||||
isLabelSyncedWithName: false,
|
||||
isNullable: true,
|
||||
isUIReadOnly: false,
|
||||
isSystem: true,
|
||||
isUnique: false,
|
||||
label: capitalize(targetFlatObjectMetadata.namePlural),
|
||||
name: targetFlatObjectMetadata.namePlural,
|
||||
objectMetadataId: sourceFlatObjectMetadata.id,
|
||||
options: null,
|
||||
relationTargetFieldMetadataId: targetFieldMetadataId,
|
||||
relationTargetObjectMetadataId: targetFlatObjectMetadata.id,
|
||||
settings: {
|
||||
relationType: RelationType.ONE_TO_MANY,
|
||||
},
|
||||
standardId,
|
||||
standardOverrides: null,
|
||||
type: FieldMetadataType.RELATION,
|
||||
universalIdentifier: sourceFieldMetadataId,
|
||||
workspaceId,
|
||||
morphId: null,
|
||||
applicationId: sourceFlatObjectMetadata.applicationId,
|
||||
};
|
||||
};
|
||||
|
||||
const generateTargetFlatFieldMetadata = ({
|
||||
sourceFlatObjectMetadata,
|
||||
targetFlatObjectMetadata,
|
||||
sourceFlatFieldMetadata,
|
||||
workspaceId,
|
||||
}: Omit<
|
||||
BuildDefaultRelationFieldsForCustomObjectArgs,
|
||||
'existingFlatObjectMetadataMaps'
|
||||
> & {
|
||||
sourceFlatObjectMetadata: FlatObjectMetadata;
|
||||
targetFlatObjectMetadata: FlatObjectMetadata;
|
||||
sourceFlatFieldMetadata: FlatFieldMetadata<MorphOrRelationFieldMetadataType>;
|
||||
}): FlatFieldMetadata<FieldMetadataType.RELATION> => {
|
||||
const customStandardFieldId =
|
||||
STANDARD_OBJECT_FIELD_IDS[
|
||||
targetFlatObjectMetadata.nameSingular as (typeof DEFAULT_RELATIONS_OBJECTS_STANDARD_IDS)[number]
|
||||
].custom;
|
||||
|
||||
const { description } = buildDescriptionForRelationFieldMetadataOnToField({
|
||||
relationObjectMetadataNamePlural: targetFlatObjectMetadata.namePlural,
|
||||
targetObjectLabelSingular: sourceFlatObjectMetadata.labelSingular,
|
||||
});
|
||||
const createdAt = new Date().toISOString();
|
||||
const standardId = createRelationDeterministicUuid({
|
||||
objectId: sourceFlatObjectMetadata.id,
|
||||
standardId: customStandardFieldId,
|
||||
});
|
||||
|
||||
return {
|
||||
morphId: null,
|
||||
calendarViewIds: [],
|
||||
mainGroupByFieldMetadataViewIds: [],
|
||||
viewFieldIds: [],
|
||||
kanbanAggregateOperationViewIds: [],
|
||||
viewFilterIds: [],
|
||||
id: sourceFlatFieldMetadata.relationTargetFieldMetadataId,
|
||||
name: sourceFlatObjectMetadata.nameSingular,
|
||||
label: sourceFlatObjectMetadata.labelSingular,
|
||||
description,
|
||||
standardId,
|
||||
objectMetadataId: targetFlatObjectMetadata.id,
|
||||
workspaceId: workspaceId,
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isSystem: true,
|
||||
isUIReadOnly: false,
|
||||
type: FieldMetadataType.RELATION,
|
||||
icon: 'IconBuildingSkyscraper',
|
||||
isNullable: true,
|
||||
settings: {
|
||||
relationType: RelationType.MANY_TO_ONE,
|
||||
onDelete: RelationOnDeleteAction.CASCADE,
|
||||
joinColumnName: `${sourceFlatObjectMetadata.nameSingular}Id`,
|
||||
},
|
||||
createdAt,
|
||||
updatedAt: createdAt,
|
||||
defaultValue: null,
|
||||
isLabelSyncedWithName: false,
|
||||
isUnique: false,
|
||||
options: null,
|
||||
relationTargetFieldMetadataId: sourceFlatFieldMetadata.id,
|
||||
relationTargetObjectMetadataId: sourceFlatObjectMetadata.id,
|
||||
standardOverrides: null,
|
||||
universalIdentifier: sourceFlatFieldMetadata.relationTargetFieldMetadataId,
|
||||
applicationId: sourceFlatObjectMetadata.applicationId,
|
||||
};
|
||||
};
|
||||
|
||||
const DEFAULT_RELATIONS_OBJECTS_STANDARD_IDS = [
|
||||
'timelineActivity',
|
||||
@@ -173,10 +30,29 @@ const DEFAULT_RELATIONS_OBJECTS_STANDARD_IDS = [
|
||||
'taskTarget',
|
||||
] as const satisfies (keyof typeof STANDARD_OBJECT_IDS)[];
|
||||
|
||||
const morphIdByRelationObjectNameSingular = {
|
||||
timelineActivity: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.targetMorphId,
|
||||
favorite: null,
|
||||
attachment: null,
|
||||
noteTarget: null,
|
||||
taskTarget: null,
|
||||
} satisfies Record<
|
||||
(typeof DEFAULT_RELATIONS_OBJECTS_STANDARD_IDS)[number],
|
||||
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_OBJECT_IDS)[];
|
||||
|
||||
export type BuildDefaultRelationFieldsForCustomObjectArgs = {
|
||||
existingFeatureFlagsMap: FeatureFlagMap;
|
||||
existingFlatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>;
|
||||
workspaceId: string;
|
||||
sourceFlatObjectMetadata: FlatObjectMetadata;
|
||||
workspaceCustomApplicationId: string;
|
||||
};
|
||||
|
||||
type SourceAndTargetFlatFieldMetadatasRecord = {
|
||||
@@ -190,9 +66,11 @@ const EMPTY_SOURCE_AND_TARGET_FLAT_FIELD_METADATAS_RECORD: SourceAndTargetFlatFi
|
||||
};
|
||||
|
||||
export const buildDefaultRelationFlatFieldMetadatasForCustomObject = ({
|
||||
existingFeatureFlagsMap,
|
||||
existingFlatObjectMetadataMaps,
|
||||
sourceFlatObjectMetadata,
|
||||
workspaceId,
|
||||
workspaceCustomApplicationId,
|
||||
}: BuildDefaultRelationFieldsForCustomObjectArgs): SourceAndTargetFlatFieldMetadatasRecord => {
|
||||
const objectIdByNameSingular = Object.values(
|
||||
existingFlatObjectMetadataMaps.byId,
|
||||
@@ -207,54 +85,109 @@ export const buildDefaultRelationFlatFieldMetadatasForCustomObject = ({
|
||||
};
|
||||
}, {});
|
||||
|
||||
return DEFAULT_RELATIONS_OBJECTS_STANDARD_IDS.reduce<SourceAndTargetFlatFieldMetadatasRecord>(
|
||||
(sourceAndTargetFlatFieldMetadatasRecord, objectMetadataNameSingular) => {
|
||||
const targetFlatObjectMetadataId =
|
||||
objectIdByNameSingular[objectMetadataNameSingular];
|
||||
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' &&
|
||||
existingFeatureFlagsMap[
|
||||
FeatureFlagKey.IS_TIMELINE_ACTIVITY_MIGRATED
|
||||
]) ??
|
||||
false;
|
||||
const isObjectMigratedToMorphRelations =
|
||||
isObjectMigratedFromOlderReleases || isFeatureFlagEnabled;
|
||||
|
||||
if (!isDefined(targetFlatObjectMetadataId)) {
|
||||
throw new ObjectMetadataException(
|
||||
`Standard target object metadata id ${targetFlatObjectMetadataId} not found in cache`,
|
||||
ObjectMetadataExceptionCode.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
const targetFlatObjectMetadataId =
|
||||
objectIdByNameSingular[objectMetadataNameSingular];
|
||||
|
||||
const targetFlatObjectMetadata = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityMaps: existingFlatObjectMetadataMaps,
|
||||
flatEntityId: targetFlatObjectMetadataId,
|
||||
});
|
||||
if (!isDefined(targetFlatObjectMetadataId)) {
|
||||
throw new ObjectMetadataException(
|
||||
`Standard target object metadata id ${targetFlatObjectMetadataId} not found in cache`,
|
||||
ObjectMetadataExceptionCode.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
|
||||
if (!isDefined(targetFlatObjectMetadata)) {
|
||||
throw new ObjectMetadataException(
|
||||
`Standard target object metadata of id ${targetFlatObjectMetadataId} not found in cache`,
|
||||
ObjectMetadataExceptionCode.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
const targetFlatObjectMetadata =
|
||||
findFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityMaps: existingFlatObjectMetadataMaps,
|
||||
flatEntityId: targetFlatObjectMetadataId,
|
||||
});
|
||||
|
||||
const sourceFlatFieldMetadata = generateSourceFlatFieldMetadata({
|
||||
sourceFlatObjectMetadata,
|
||||
targetFlatObjectMetadata,
|
||||
workspaceId,
|
||||
});
|
||||
const standardId =
|
||||
CUSTOM_OBJECT_STANDARD_FIELD_IDS[
|
||||
targetFlatObjectMetadata.namePlural as keyof typeof CUSTOM_OBJECT_STANDARD_FIELD_IDS
|
||||
];
|
||||
|
||||
const targetFlatFieldMetadata = generateTargetFlatFieldMetadata({
|
||||
sourceFlatFieldMetadata,
|
||||
sourceFlatObjectMetadata,
|
||||
targetFlatObjectMetadata,
|
||||
workspaceId,
|
||||
});
|
||||
if (!isDefined(standardId)) {
|
||||
throw new ObjectMetadataException(
|
||||
`Standard field ID not found for target object ${targetFlatObjectMetadata.namePlural}`,
|
||||
ObjectMetadataExceptionCode.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
standardSourceFlatFieldMetadatas: [
|
||||
...sourceAndTargetFlatFieldMetadatasRecord.standardSourceFlatFieldMetadatas,
|
||||
sourceFlatFieldMetadata,
|
||||
],
|
||||
standardTargetFlatFieldMetadatas: [
|
||||
...sourceAndTargetFlatFieldMetadatasRecord.standardTargetFlatFieldMetadatas,
|
||||
targetFlatFieldMetadata,
|
||||
],
|
||||
};
|
||||
},
|
||||
EMPTY_SOURCE_AND_TARGET_FLAT_FIELD_METADATAS_RECORD,
|
||||
);
|
||||
const icon =
|
||||
STANDARD_OBJECT_ICONS[
|
||||
targetFlatObjectMetadata.nameSingular as keyof typeof STANDARD_OBJECT_ICONS
|
||||
] || 'IconBuildingSkyscraper';
|
||||
|
||||
const morphFieldName = `target${capitalize(sourceFlatObjectMetadata.nameSingular)}`;
|
||||
const fieldName = isObjectMigratedToMorphRelations
|
||||
? morphFieldName
|
||||
: sourceFlatObjectMetadata.nameSingular;
|
||||
const joinColumnName = computeMorphOrRelationFieldJoinColumnName({
|
||||
name: fieldName,
|
||||
});
|
||||
|
||||
const morphId =
|
||||
morphIdByRelationObjectNameSingular[objectMetadataNameSingular];
|
||||
|
||||
const { flatFieldMetadatas } =
|
||||
generateMorphOrRelationFlatFieldMetadataPair({
|
||||
sourceFlatObjectMetadata,
|
||||
targetFlatObjectMetadata,
|
||||
targetFlatFieldMetadataType: isObjectMigratedToMorphRelations
|
||||
? FieldMetadataType.MORPH_RELATION
|
||||
: FieldMetadataType.RELATION,
|
||||
workspaceId,
|
||||
workspaceCustomApplicationId,
|
||||
sourceFlatObjectMetadataJoinColumnName: joinColumnName,
|
||||
morphId,
|
||||
targetFieldName: fieldName,
|
||||
createFieldInput: {
|
||||
icon: 'IconBuildingSkyscraper',
|
||||
type: FieldMetadataType.RELATION,
|
||||
name: targetFlatObjectMetadata.namePlural,
|
||||
label: capitalize(targetFlatObjectMetadata.labelPlural),
|
||||
objectMetadataId: sourceFlatObjectMetadata.id,
|
||||
standardId,
|
||||
relationCreationPayload: {
|
||||
type: RelationType.ONE_TO_MANY,
|
||||
targetObjectMetadataId: targetFlatObjectMetadata.id,
|
||||
targetFieldLabel: capitalize(
|
||||
sourceFlatObjectMetadata.nameSingular,
|
||||
),
|
||||
targetFieldIcon: icon,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
standardSourceFlatFieldMetadatas: [
|
||||
...sourceAndTargetFlatFieldMetadatasRecord.standardSourceFlatFieldMetadatas,
|
||||
flatFieldMetadatas[0],
|
||||
],
|
||||
standardTargetFlatFieldMetadatas: [
|
||||
...sourceAndTargetFlatFieldMetadatasRecord.standardTargetFlatFieldMetadatas,
|
||||
flatFieldMetadatas[1],
|
||||
],
|
||||
};
|
||||
},
|
||||
EMPTY_SOURCE_AND_TARGET_FLAT_FIELD_METADATAS_RECORD,
|
||||
);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user