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 :

![plan](https://github.com/user-attachments/assets/51c63ea6-fb0d-40b4-b99d-3e0b35a204e6)
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:
Guillim
2025-12-11 14:28:26 +01:00
committed by GitHub
parent c13ef0c4a4
commit b83dc3aaff
65 changed files with 1299 additions and 467 deletions
@@ -0,0 +1,330 @@
import { type EachTestingContext } from 'twenty-shared/testing';
import {
type FieldMetadataRelationSettings,
FieldMetadataType,
RelationOnDeleteAction,
RelationType,
} from 'twenty-shared/types';
import {
generateMorphOrRelationFlatFieldMetadataPair,
type SourceTargetMorphOrRelationFlatFieldAndFlatIndex,
} from 'src/engine/metadata-modules/flat-field-metadata/utils/generate-morph-or-relation-flat-field-metadata-pair.util';
import { COMPANY_FLAT_OBJECT_MOCK } from 'src/engine/metadata-modules/flat-object-metadata/__mocks__/company-flat-object.mock';
import { PET_FLAT_OBJECT_MOCK } from 'src/engine/metadata-modules/flat-object-metadata/__mocks__/pet-flat-object.mock';
type GenerateMorphOrRelationFlatFieldMetadataPairTestInput = Parameters<
typeof generateMorphOrRelationFlatFieldMetadataPair
>[0];
type TestCase = EachTestingContext<{
input: GenerateMorphOrRelationFlatFieldMetadataPairTestInput;
expectedSourceFieldType: FieldMetadataType;
expectedTargetFieldType: FieldMetadataType;
expectedSourceRelationType: RelationType;
expectedTargetRelationType: RelationType;
shouldSourceHaveMorphId: boolean;
}>;
describe('generate Morph Or Relation Flat Field Metadata Pair test suite', () => {
const mockWorkspaceId = '20202020-1c25-4d02-bf25-6aeccf7ea419';
const mockWorkspaceCustomApplicationId =
'20202020-81ee-42da-a281-668632f32fe7';
describe('Success cases', () => {
const testCases: TestCase[] = [
{
title:
'should generate a regular RELATION field pair with ONE_TO_MANY relation type',
context: {
input: {
sourceFlatObjectMetadata: COMPANY_FLAT_OBJECT_MOCK,
targetFlatObjectMetadata: PET_FLAT_OBJECT_MOCK,
targetFlatFieldMetadataType: FieldMetadataType.RELATION,
sourceFlatObjectMetadataJoinColumnName: 'petId',
workspaceId: mockWorkspaceId,
workspaceCustomApplicationId: mockWorkspaceCustomApplicationId,
createFieldInput: {
name: 'pets',
label: 'Pets',
description: 'Company pets',
icon: 'IconCat',
type: FieldMetadataType.RELATION,
objectMetadataId: COMPANY_FLAT_OBJECT_MOCK.id,
isCustom: true,
isSystem: false,
isUnique: false,
relationCreationPayload: {
type: RelationType.ONE_TO_MANY,
targetObjectMetadataId: PET_FLAT_OBJECT_MOCK.id,
targetFieldLabel: 'Company',
targetFieldIcon: 'IconBuildingSkyscraper',
},
},
},
expectedSourceFieldType: FieldMetadataType.RELATION,
expectedTargetFieldType: FieldMetadataType.RELATION,
expectedSourceRelationType: RelationType.ONE_TO_MANY,
expectedTargetRelationType: RelationType.MANY_TO_ONE,
shouldSourceHaveMorphId: false,
},
},
{
title:
'should generate a regular RELATION field pair with MANY_TO_ONE relation type',
context: {
input: {
sourceFlatObjectMetadata: COMPANY_FLAT_OBJECT_MOCK,
targetFlatObjectMetadata: PET_FLAT_OBJECT_MOCK,
targetFlatFieldMetadataType: FieldMetadataType.RELATION,
sourceFlatObjectMetadataJoinColumnName: 'petId',
workspaceId: mockWorkspaceId,
workspaceCustomApplicationId: mockWorkspaceCustomApplicationId,
createFieldInput: {
name: 'pets',
label: 'Pets',
description: 'Company pets',
icon: 'IconCat',
type: FieldMetadataType.RELATION,
objectMetadataId: COMPANY_FLAT_OBJECT_MOCK.id,
isCustom: true,
isSystem: false,
isUnique: false,
relationCreationPayload: {
type: RelationType.MANY_TO_ONE,
targetObjectMetadataId: PET_FLAT_OBJECT_MOCK.id,
targetFieldLabel: 'Company',
targetFieldIcon: 'IconBuildingSkyscraper',
},
},
},
expectedSourceFieldType: FieldMetadataType.RELATION,
expectedTargetFieldType: FieldMetadataType.RELATION,
expectedSourceRelationType: RelationType.MANY_TO_ONE,
expectedTargetRelationType: RelationType.ONE_TO_MANY,
shouldSourceHaveMorphId: false,
},
},
{
title:
'should generate a regular RELATION with isCustom, isSytem, isUnique not default values',
context: {
input: {
sourceFlatObjectMetadata: COMPANY_FLAT_OBJECT_MOCK,
targetFlatObjectMetadata: PET_FLAT_OBJECT_MOCK,
targetFlatFieldMetadataType: FieldMetadataType.RELATION,
sourceFlatObjectMetadataJoinColumnName: 'petId',
workspaceId: mockWorkspaceId,
workspaceCustomApplicationId: mockWorkspaceCustomApplicationId,
createFieldInput: {
name: 'pets',
label: 'Pets',
description: 'Company pets',
icon: 'IconCat',
type: FieldMetadataType.RELATION,
objectMetadataId: COMPANY_FLAT_OBJECT_MOCK.id,
isCustom: false,
isSystem: true,
isUnique: true,
relationCreationPayload: {
type: RelationType.MANY_TO_ONE,
targetObjectMetadataId: PET_FLAT_OBJECT_MOCK.id,
targetFieldLabel: 'Company',
targetFieldIcon: 'IconBuildingSkyscraper',
},
},
},
expectedSourceFieldType: FieldMetadataType.RELATION,
expectedTargetFieldType: FieldMetadataType.RELATION,
expectedSourceRelationType: RelationType.MANY_TO_ONE,
expectedTargetRelationType: RelationType.ONE_TO_MANY,
shouldSourceHaveMorphId: false,
},
},
{
title:
'should generate a MORPH_RELATION (MANY_TO_ONE) field pair with morphId when provided',
context: {
input: {
sourceFlatObjectMetadata: COMPANY_FLAT_OBJECT_MOCK,
targetFlatObjectMetadata: PET_FLAT_OBJECT_MOCK,
targetFlatFieldMetadataType: FieldMetadataType.RELATION,
sourceFlatObjectMetadataJoinColumnName: 'targetPetId',
workspaceId: mockWorkspaceId,
workspaceCustomApplicationId: mockWorkspaceCustomApplicationId,
morphId: '20202020-9a2b-4c3d-a4e5-f6a7b8c9d0e1',
createFieldInput: {
name: 'targetPet',
label: 'Target Pet',
description: 'Morph relation to pet',
icon: 'IconCat',
type: FieldMetadataType.MORPH_RELATION,
objectMetadataId: COMPANY_FLAT_OBJECT_MOCK.id,
isCustom: false,
isSystem: true,
isUnique: false,
relationCreationPayload: {
type: RelationType.MANY_TO_ONE,
targetObjectMetadataId: PET_FLAT_OBJECT_MOCK.id,
targetFieldLabel: 'Companies',
targetFieldIcon: 'IconBuildingSkyscraper',
},
},
},
expectedSourceFieldType: FieldMetadataType.MORPH_RELATION,
expectedTargetFieldType: FieldMetadataType.RELATION,
expectedSourceRelationType: RelationType.MANY_TO_ONE,
expectedTargetRelationType: RelationType.ONE_TO_MANY,
shouldSourceHaveMorphId: true,
},
},
{
title:
'should generate a MORPH_RELATION (ONE_TO_MANY) field pair with morphId when provided',
context: {
input: {
sourceFlatObjectMetadata: COMPANY_FLAT_OBJECT_MOCK,
targetFlatObjectMetadata: PET_FLAT_OBJECT_MOCK,
targetFlatFieldMetadataType: FieldMetadataType.RELATION,
sourceFlatObjectMetadataJoinColumnName: 'targetPetId',
workspaceId: mockWorkspaceId,
workspaceCustomApplicationId: mockWorkspaceCustomApplicationId,
morphId: '20202020-9a2b-4c3d-a4e5-f6a7b8c9d0e1',
createFieldInput: {
name: 'targetPet',
label: 'Target Pet',
description: 'Morph relation to pet',
icon: 'IconCat',
type: FieldMetadataType.MORPH_RELATION,
objectMetadataId: COMPANY_FLAT_OBJECT_MOCK.id,
isCustom: false,
isSystem: true,
isUnique: false,
relationCreationPayload: {
type: RelationType.ONE_TO_MANY,
targetObjectMetadataId: PET_FLAT_OBJECT_MOCK.id,
targetFieldLabel: 'Companies',
targetFieldIcon: 'IconBuildingSkyscraper',
},
},
},
expectedSourceFieldType: FieldMetadataType.MORPH_RELATION,
expectedTargetFieldType: FieldMetadataType.RELATION,
expectedSourceRelationType: RelationType.ONE_TO_MANY,
expectedTargetRelationType: RelationType.MANY_TO_ONE,
shouldSourceHaveMorphId: true,
},
},
{
title:
'should generate a MORPH_RELATION (ONE_TO_MANY) field pair with MORPH as target',
context: {
input: {
sourceFlatObjectMetadata: COMPANY_FLAT_OBJECT_MOCK,
targetFlatObjectMetadata: PET_FLAT_OBJECT_MOCK,
targetFlatFieldMetadataType: FieldMetadataType.MORPH_RELATION,
sourceFlatObjectMetadataJoinColumnName: 'targetPetId',
workspaceId: mockWorkspaceId,
workspaceCustomApplicationId: mockWorkspaceCustomApplicationId,
morphId: '20202020-9a2b-4c3d-a4e5-f6a7b8c9d0e1',
createFieldInput: {
name: 'targetPet',
label: 'Target Pet',
description: 'Morph relation to pet',
icon: 'IconCat',
type: FieldMetadataType.RELATION,
objectMetadataId: COMPANY_FLAT_OBJECT_MOCK.id,
isCustom: false,
isSystem: true,
isUnique: false,
relationCreationPayload: {
type: RelationType.ONE_TO_MANY,
targetObjectMetadataId: PET_FLAT_OBJECT_MOCK.id,
targetFieldLabel: 'Companies',
targetFieldIcon: 'IconBuildingSkyscraper',
},
},
},
expectedSourceFieldType: FieldMetadataType.RELATION,
expectedTargetFieldType: FieldMetadataType.MORPH_RELATION,
expectedSourceRelationType: RelationType.ONE_TO_MANY,
expectedTargetRelationType: RelationType.MANY_TO_ONE,
shouldSourceHaveMorphId: false,
},
},
];
test.each(testCases)(
'$title',
({
context: {
input,
expectedSourceFieldType,
expectedTargetFieldType,
expectedSourceRelationType,
expectedTargetRelationType,
shouldSourceHaveMorphId,
},
}) => {
const result: SourceTargetMorphOrRelationFlatFieldAndFlatIndex =
generateMorphOrRelationFlatFieldMetadataPair(input);
expect(result.flatFieldMetadatas).toHaveLength(2);
expect(result.indexMetadatas).toHaveLength(1);
const [sourceFieldMetadata, targetFieldMetadata] =
result.flatFieldMetadatas;
expect(sourceFieldMetadata.type).toBe(expectedSourceFieldType);
expect(sourceFieldMetadata.name).toBe(input.createFieldInput.name);
expect(sourceFieldMetadata.label).toBe(input.createFieldInput.label);
expect(sourceFieldMetadata.objectMetadataId).toBe(
input.sourceFlatObjectMetadata.id,
);
expect(sourceFieldMetadata.relationTargetObjectMetadataId).toBe(
input.targetFlatObjectMetadata.id,
);
const sourceSettings =
sourceFieldMetadata.settings as FieldMetadataRelationSettings;
const targetSettings =
targetFieldMetadata.settings as FieldMetadataRelationSettings;
expect(sourceSettings.relationType).toBe(expectedSourceRelationType);
if (shouldSourceHaveMorphId) {
expect(sourceFieldMetadata.morphId).toBe(input.morphId);
} else {
expect(sourceFieldMetadata.morphId).toBeNull();
}
expect(targetFieldMetadata.type).toBe(expectedTargetFieldType);
expect(targetFieldMetadata.objectMetadataId).toBe(
input.targetFlatObjectMetadata.id,
);
expect(targetFieldMetadata.relationTargetObjectMetadataId).toBe(
input.sourceFlatObjectMetadata.id,
);
expect(targetSettings.relationType).toBe(expectedTargetRelationType);
expect(sourceFieldMetadata.relationTargetFieldMetadataId).toBe(
targetFieldMetadata.id,
);
expect(targetFieldMetadata.relationTargetFieldMetadataId).toBe(
sourceFieldMetadata.id,
);
if (expectedSourceRelationType === RelationType.MANY_TO_ONE) {
expect(sourceSettings.joinColumnName).toBe(
input.sourceFlatObjectMetadataJoinColumnName,
);
expect(sourceSettings.onDelete).toBe(RelationOnDeleteAction.SET_NULL);
}
if (expectedTargetRelationType === RelationType.MANY_TO_ONE) {
expect(targetSettings.joinColumnName).toBeDefined();
expect(targetSettings.onDelete).toBe(RelationOnDeleteAction.SET_NULL);
}
},
);
});
});
@@ -99,6 +99,7 @@ export const computeFlatFieldToUpdateFromMorphRelationUpdatePayload = ({
},
sourceFlatObjectMetadata: sourceObjectMetadata,
targetFlatObjectMetadata: newTargetObjectMetadata,
targetFlatFieldMetadataType: FieldMetadataType.RELATION,
workspaceId: fieldMetadataToUpdate.workspaceId,
workspaceCustomApplicationId,
sourceFlatObjectMetadataJoinColumnName:
@@ -1,5 +1,5 @@
import { msg } from '@lingui/core/macro';
import { type FieldMetadataType } from 'twenty-shared/types';
import { FieldMetadataType } from 'twenty-shared/types';
import { computeMorphRelationFieldName, isDefined } from 'twenty-shared/utils';
import { v4 } from 'uuid';
@@ -92,6 +92,7 @@ export const fromMorphRelationCreateFieldInputToFlatFieldMetadatas = async ({
sourceFlatObjectMetadataJoinColumnName,
sourceFlatObjectMetadata,
targetFlatObjectMetadata,
targetFlatFieldMetadataType: FieldMetadataType.RELATION,
workspaceId,
morphId,
workspaceCustomApplicationId,
@@ -1,5 +1,5 @@
import { msg } from '@lingui/core/macro';
import { type FieldMetadataType } from 'twenty-shared/types';
import { FieldMetadataType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { type CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input';
@@ -70,6 +70,7 @@ export const fromRelationCreateFieldInputToFlatFieldMetadatas = async ({
}),
sourceFlatObjectMetadata,
targetFlatObjectMetadata,
targetFlatFieldMetadataType: FieldMetadataType.RELATION,
workspaceId,
workspaceCustomApplicationId,
});
@@ -14,6 +14,8 @@ import { generateIndexForFlatFieldMetadata } from 'src/engine/metadata-modules/f
import { getDefaultFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/get-default-flat-field-metadata-from-create-field-input.util';
import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
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';
type ComputeFieldMetadataRelationSettingsForRelationTypeArgs = {
relationType: RelationType;
@@ -39,6 +41,7 @@ const computeFieldMetadataRelationSettingsForRelationType = ({
type GenerateMorphOrRelationFlatFieldMetadataPairArgs = {
targetFlatObjectMetadata: FlatObjectMetadata;
sourceFlatObjectMetadata: FlatObjectMetadata;
targetFlatFieldMetadataType: FieldMetadataType;
sourceFlatObjectMetadataJoinColumnName: string;
createFieldInput: Omit<CreateFieldInput, 'workspaceId'> &
Required<
@@ -59,12 +62,15 @@ export const generateMorphOrRelationFlatFieldMetadataPair = ({
createFieldInput,
sourceFlatObjectMetadata,
targetFlatObjectMetadata,
targetFlatFieldMetadataType,
workspaceId,
workspaceCustomApplicationId,
sourceFlatObjectMetadataJoinColumnName,
morphId = null,
targetFieldName,
}: GenerateMorphOrRelationFlatFieldMetadataPairArgs): SourceTargetMorphOrRelationFlatFieldAndFlatIndex => {
const sourceFlatFieldMetadataType = createFieldInput.type;
const { relationCreationPayload } = createFieldInput;
const sourceFlatFieldMetadataSettings =
@@ -74,6 +80,17 @@ export const generateMorphOrRelationFlatFieldMetadataPair = ({
});
const targetRelationTargetFieldMetadataId = v4();
const sourceRelationTargetFieldMetadataId = v4();
const defaultDescriptionFromField =
buildDescriptionForRelationFieldMetadataOnFromField({
relationObjectMetadataNamePlural: sourceFlatObjectMetadata.namePlural,
targetObjectLabelSingular: targetFlatObjectMetadata.labelSingular,
});
const defaultDescriptionToField =
buildDescriptionForRelationFieldMetadataOnToField({
relationObjectMetadataNamePlural: targetFlatObjectMetadata.namePlural,
targetObjectLabelSingular: sourceFlatObjectMetadata.labelSingular,
});
const sourceFlatFieldMetadata: Omit<
FlatFieldMetadata<MorphOrRelationFieldMetadataType>,
'flatRelationTargetFieldMetadata'
@@ -84,10 +101,15 @@ export const generateMorphOrRelationFlatFieldMetadataPair = ({
fieldMetadataId: sourceRelationTargetFieldMetadataId,
workspaceCustomApplicationId,
}),
morphId,
morphId:
sourceFlatFieldMetadataType === FieldMetadataType.MORPH_RELATION
? morphId
: null,
objectMetadataId: sourceFlatObjectMetadata.id,
icon: createFieldInput.icon ?? 'IconRelationOneToMany',
type: createFieldInput.type,
type: sourceFlatFieldMetadataType,
description:
createFieldInput.description ?? defaultDescriptionFromField.description,
defaultValue: null,
settings: sourceFlatFieldMetadataSettings,
options: null,
@@ -97,6 +119,8 @@ export const generateMorphOrRelationFlatFieldMetadataPair = ({
const targetCreateFieldInput: CreateFieldInput = {
icon: relationCreationPayload.targetFieldIcon ?? 'Icon123',
description:
createFieldInput.description ?? defaultDescriptionToField.description,
label: relationCreationPayload.targetFieldLabel,
name:
targetFieldName ??
@@ -117,21 +141,27 @@ export const generateMorphOrRelationFlatFieldMetadataPair = ({
? RelationType.MANY_TO_ONE
: RelationType.ONE_TO_MANY,
});
const targetFlatFieldMetadata: FlatFieldMetadata<FieldMetadataType.RELATION> =
{
...getDefaultFlatFieldMetadata({
createFieldInput: targetCreateFieldInput,
workspaceId,
fieldMetadataId: targetRelationTargetFieldMetadataId,
workspaceCustomApplicationId,
}),
type: FieldMetadataType.RELATION,
defaultValue: null,
settings: targetFlatFieldMetadataSettings,
options: null,
relationTargetFieldMetadataId: sourceRelationTargetFieldMetadataId,
relationTargetObjectMetadataId: sourceFlatObjectMetadata.id,
};
const targetFlatFieldMetadata: FlatFieldMetadata<
typeof targetFlatFieldMetadataType
> = {
...getDefaultFlatFieldMetadata({
createFieldInput: targetCreateFieldInput,
workspaceId,
fieldMetadataId: targetRelationTargetFieldMetadataId,
workspaceCustomApplicationId,
}),
morphId:
targetFlatFieldMetadataType === FieldMetadataType.MORPH_RELATION
? morphId
: null,
type: targetFlatFieldMetadataType,
defaultValue: null,
settings: targetFlatFieldMetadataSettings,
options: null,
relationTargetFieldMetadataId: sourceRelationTargetFieldMetadataId,
relationTargetObjectMetadataId: sourceFlatObjectMetadata.id,
};
const indexMetadata: FlatIndexMetadata = generateIndexForFlatFieldMetadata({
flatFieldMetadata:
@@ -39,7 +39,7 @@ export const getDefaultFlatFieldMetadata = ({
createFieldInput.isRemoteCreation,
),
isSystem: false,
isUnique: createFieldInput.isUnique ?? null,
isUnique: createFieldInput.isUnique ?? false,
label: createFieldInput.label,
name: createFieldInput.name,
objectMetadataId: createFieldInput.objectMetadataId,