diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index ec2f046a778..77623999a07 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -1307,6 +1307,7 @@ export enum FeatureFlagKey { IS_PUBLIC_DOMAIN_ENABLED = 'IS_PUBLIC_DOMAIN_ENABLED', IS_RECORD_PAGE_LAYOUT_ENABLED = 'IS_RECORD_PAGE_LAYOUT_ENABLED', IS_STRIPE_INTEGRATION_ENABLED = 'IS_STRIPE_INTEGRATION_ENABLED', + IS_TIMELINE_ACTIVITY_MIGRATED = 'IS_TIMELINE_ACTIVITY_MIGRATED', IS_UNIQUE_INDEXES_ENABLED = 'IS_UNIQUE_INDEXES_ENABLED', IS_WORKFLOW_RUN_STOPPAGE_ENABLED = 'IS_WORKFLOW_RUN_STOPPAGE_ENABLED' } diff --git a/packages/twenty-front/src/generated/graphql.ts b/packages/twenty-front/src/generated/graphql.ts index 661a37b2185..444684dea77 100644 --- a/packages/twenty-front/src/generated/graphql.ts +++ b/packages/twenty-front/src/generated/graphql.ts @@ -1290,6 +1290,7 @@ export enum FeatureFlagKey { IS_PUBLIC_DOMAIN_ENABLED = 'IS_PUBLIC_DOMAIN_ENABLED', IS_RECORD_PAGE_LAYOUT_ENABLED = 'IS_RECORD_PAGE_LAYOUT_ENABLED', IS_STRIPE_INTEGRATION_ENABLED = 'IS_STRIPE_INTEGRATION_ENABLED', + IS_TIMELINE_ACTIVITY_MIGRATED = 'IS_TIMELINE_ACTIVITY_MIGRATED', IS_UNIQUE_INDEXES_ENABLED = 'IS_UNIQUE_INDEXES_ENABLED', IS_WORKFLOW_RUN_STOPPAGE_ENABLED = 'IS_WORKFLOW_RUN_STOPPAGE_ENABLED' } diff --git a/packages/twenty-front/src/modules/activities/timeline-activities/hooks/useTimelineActivities.ts b/packages/twenty-front/src/modules/activities/timeline-activities/hooks/useTimelineActivities.ts index 78111410cba..581fcd81ee7 100644 --- a/packages/twenty-front/src/modules/activities/timeline-activities/hooks/useTimelineActivities.ts +++ b/packages/twenty-front/src/modules/activities/timeline-activities/hooks/useTimelineActivities.ts @@ -5,15 +5,23 @@ import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getActivi import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { useGenerateDepthRecordGqlFieldsFromObject } from '@/object-record/graphql/record-gql-fields/hooks/useGenerateDepthRecordGqlFieldsFromObject'; import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; -import { isDefined } from 'twenty-shared/utils'; +import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; +import { capitalize, isDefined } from 'twenty-shared/utils'; +import { FeatureFlagKey } from '~/generated/graphql'; // do we need to test this? export const useTimelineActivities = ( targetableObject: ActivityTargetableObject, ) => { - const targetableObjectFieldIdName = getActivityTargetObjectFieldIdName({ - nameSingular: targetableObject.targetObjectNameSingular, - }); + const isTimelineActivityMigrated = useIsFeatureEnabled( + FeatureFlagKey.IS_TIMELINE_ACTIVITY_MIGRATED, + ); + + const targetableObjectFieldIdName = isTimelineActivityMigrated + ? `target${capitalize(targetableObject.targetObjectNameSingular)}Id` + : getActivityTargetObjectFieldIdName({ + nameSingular: targetableObject.targetObjectNameSingular, + }); const { recordGqlFields: depthOneRecordGqlFields } = useGenerateDepthRecordGqlFieldsFromObject({ diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-13/1-13-migrate-timeline-activity-to-morph-relations.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-13/1-13-migrate-timeline-activity-to-morph-relations.command.ts new file mode 100644 index 00000000000..df579e6e31f --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-13/1-13-migrate-timeline-activity-to-morph-relations.command.ts @@ -0,0 +1,267 @@ +import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; + +import { Command } from 'nest-commander'; +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 { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; +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 { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.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 { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util'; +import { TIMELINE_ACTIVITY_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids'; +import { buildTimelineActivityRelatedMorphFieldMetadataName } from 'src/modules/timeline/utils/timeline-activity-related-morph-field-metadata-name-builder.util'; + +@Command({ + name: 'upgrade:1-13:migrate-timeline-activity-to-morph-relations', + description: + 'Migrate timeline activity relations to morph relation fields and set feature flag', +}) +export class MigrateTimelineActivityToMorphRelationsCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner { + constructor( + @InjectRepository(WorkspaceEntity) + protected readonly workspaceRepository: Repository, + private readonly featureFlagService: FeatureFlagService, + @InjectRepository(ObjectMetadataEntity) + private readonly objectMetadataRepository: Repository, + @InjectDataSource() + private readonly coreDataSource: DataSource, + private readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager, + protected readonly dataSourceService: DataSourceService, + private readonly workspaceCacheStorageService: WorkspaceCacheStorageService, + private readonly workspaceCacheService: WorkspaceCacheService, + private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService, + ) { + super(workspaceRepository, twentyORMGlobalManager, dataSourceService); + } + + override async runOnWorkspace({ + workspaceId, + options, + }: RunOnWorkspaceArgs): Promise { + const isMigrated = await this.featureFlagService.isFeatureEnabled( + FeatureFlagKey.IS_TIMELINE_ACTIVITY_MIGRATED, + workspaceId, + ); + + this.logger.log(`Migrating timelineActivity for workspace ${workspaceId}`); + + if (isMigrated) { + this.logger.log( + `Timeline activity migration already completed. Skipping...`, + ); + + return; + } + + if (options.dryRun) { + this.logger.log( + `Would have migrated timeline activities for workspace ${workspaceId}. Skipping...`, + ); + + return; + } + + const queryRunner = this.coreDataSource.createQueryRunner(); + + await queryRunner.connect(); + await queryRunner.startTransaction(); + + try { + const schemaName = getWorkspaceSchemaName(workspaceId); + const tableName = 'timelineActivity'; + + const customObjectMetadata = await this.objectMetadataRepository.find({ + where: { + workspaceId, + isCustom: true, + }, + }); + const customObjectMetadataNames = customObjectMetadata.map( + (objectMetadata) => objectMetadata.nameSingular, + ); + + const fieldNameMigrations = [ + { old: 'company', new: 'targetCompany' }, + { old: 'person', new: 'targetPerson' }, + { old: 'opportunity', new: 'targetOpportunity' }, + { old: 'note', new: 'targetNote' }, + { old: 'task', new: 'targetTask' }, + { old: 'workflow', new: 'targetWorkflow' }, + { old: 'workflowVersion', new: 'targetWorkflowVersion' }, + { old: 'workflowRun', new: 'targetWorkflowRun' }, + { old: 'dashboard', new: 'targetDashboard' }, + ...customObjectMetadataNames.map((customObjectName) => ({ + old: `${customObjectName}`, + new: `target${capitalize(customObjectName)}`, + })), + ]; + + const fieldMigrations = fieldNameMigrations.map( + ({ old: oldFieldName, new: newFieldName }) => ({ + old: `${oldFieldName}Id`, + new: `${newFieldName}Id`, + }), + ); + + const timelineActivityObjectMetadata = + await this.objectMetadataRepository.findOne({ + where: { + workspaceId, + nameSingular: 'timelineActivity', + }, + relations: ['fields'], + }); + + if (!timelineActivityObjectMetadata) { + this.logger.error( + `🟥 Timeline activity object metadata not found for workspace ${workspaceId}`, + ); + + return; + } + + for (const { new: newField, old: oldField } of fieldMigrations) { + try { + await queryRunner.query( + `ALTER TABLE "${schemaName}"."${tableName}" + RENAME COLUMN "${oldField}" TO "${newField}"`, + ); + this.logger.log( + `Renamed column "${oldField}" to "${newField}" for "${tableName}"`, + ); + } catch (error) { + this.logger.error( + `Error renaming column "${oldField}" to "${newField}" for "${tableName}" in workspace ${workspaceId}`, + error, + ); + + throw error; + } + } + + this.logger.log(`✅ Successfully migrated timeline activity records`); + + const objectNamesToMigrate = fieldNameMigrations.map( + ({ old: oldFieldName }) => oldFieldName, + ); + + const relatedObjectMetadata = await this.objectMetadataRepository.find({ + where: { + workspaceId, + }, + }); + + const relatedObjectMetadataMap = new Map( + relatedObjectMetadata.map((obj) => [obj.nameSingular, obj]), + ); + + const morphId = TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.targetMorphId; + + for (const objectName of objectNamesToMigrate) { + const relatedObject = relatedObjectMetadataMap.get(objectName); + + if (!relatedObject) { + this.logger.warn( + `Related object metadata not found for ${objectName} in workspace ${workspaceId}`, + ); + continue; + } + + const fieldToMigrate = timelineActivityObjectMetadata.fields.find( + (field) => + field.type === 'RELATION' && + field.relationTargetObjectMetadataId === relatedObject.id, + ); + + if (!fieldToMigrate) { + this.logger.log( + `No RELATION field found for ${objectName} in timelineActivity`, + ); + continue; + } + + const newFieldName = buildTimelineActivityRelatedMorphFieldMetadataName( + fieldToMigrate.name, + ); + + const settings = { + ...fieldToMigrate.settings, + joinColumnName: computeMorphOrRelationFieldJoinColumnName({ + name: newFieldName, + }), + }; + + try { + const result = await queryRunner.query( + `UPDATE core."fieldMetadata" + SET name = $1, type = 'MORPH_RELATION', "morphId" = $3, settings = $4 + WHERE id = $2`, + [newFieldName, fieldToMigrate.id, morphId, settings], + ); + + 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 ${objectName} in workspace ${workspaceId}`, + error, + ); + + throw error; + } + } + + this.logger.log( + `✅ Successfully migrated timeline activity fieldmetadata`, + ); + + await queryRunner.commitTransaction(); + + await this.featureFlagService.enableFeatureFlags( + [FeatureFlagKey.IS_TIMELINE_ACTIVITY_MIGRATED], + workspaceId, + ); + + await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [ + 'flatFieldMetadataMaps', + 'flatObjectMetadataMaps', + 'featureFlagsMap', + ]); + + await this.workspaceMetadataVersionService.incrementMetadataVersion( + workspaceId, + ); + this.logger.log(`Cache flushed`); + + this.logger.log( + `Set IS_TIMELINE_ACTIVITY_MIGRATED feature flag for workspace ${workspaceId}`, + ); + + this.logger.log(`Flush cache for workspace ${workspaceId}`); + await this.workspaceCacheStorageService.flush(workspaceId); + } catch (error) { + await queryRunner.rollbackTransaction(); + this.logger.error( + `Error migrating timeline activity to morph relations (rolled transaction back on ${workspaceId})`, + error, + ); + throw error; + } finally { + await queryRunner.release(); + } + } +} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-13/1-13-upgrade-version-command.module.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-13/1-13-upgrade-version-command.module.ts index c3b4b954bf4..118f46ad277 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-13/1-13-upgrade-version-command.module.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-13/1-13-upgrade-version-command.module.ts @@ -6,23 +6,30 @@ import { BackfillViewMainGroupByFieldMetadataIdCommand } from 'src/database/comm import { CleanEmptyStringNullInTextFieldsCommand } from 'src/database/commands/upgrade-version-command/1-13/1-13-clean-empty-string-null-in-text-fields.command'; import { DeduplicateRoleTargetsCommand } from 'src/database/commands/upgrade-version-command/1-13/1-13-deduplicate-role-targets.command'; import { MigrateStandardInvalidEntitiesCommand } from 'src/database/commands/upgrade-version-command/1-13/1-13-migrate-standard-invalid-entities.command'; +import { MigrateTimelineActivityToMorphRelationsCommand } from 'src/database/commands/upgrade-version-command/1-13/1-13-migrate-timeline-activity-to-morph-relations.command'; import { RenameIndexNameCommand } from 'src/database/commands/upgrade-version-command/1-13/1-13-rename-unique-index.command'; import { UpdateRoleTargetsUniqueConstraintMigrationCommand } from 'src/database/commands/upgrade-version-command/1-13/1-13-update-role-targets-unique-constraint-migration.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 { 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 { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.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 { PageLayoutTabEntity } from 'src/engine/metadata-modules/page-layout/entities/page-layout-tab.entity'; import { PageLayoutWidgetEntity } from 'src/engine/metadata-modules/page-layout/entities/page-layout-widget.entity'; import { PageLayoutEntity } from 'src/engine/metadata-modules/page-layout/entities/page-layout.entity'; import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity'; import { ViewGroupEntity } from 'src/engine/metadata-modules/view-group/entities/view-group.entity'; import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity'; +import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module'; +import { WorkspaceSchemaManagerModule } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.module'; +import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module'; import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module'; +import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-objects/timeline-activity.workspace-entity'; @Module({ imports: [ @@ -38,11 +45,17 @@ import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache PageLayoutEntity, PageLayoutWidgetEntity, PageLayoutTabEntity, + TimelineActivityWorkspaceEntity, ]), DataSourceModule, - ApplicationModule, + WorkspaceSchemaManagerModule, + WorkspaceCacheStorageModule, + WorkspaceMetadataVersionModule, + FeatureFlagModule, WorkspaceCacheModule, FieldMetadataModule, + ObjectMetadataModule, + ApplicationModule, ], providers: [ CleanEmptyStringNullInTextFieldsCommand, @@ -52,6 +65,7 @@ import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache RenameIndexNameCommand, MigrateStandardInvalidEntitiesCommand, UpdateRoleTargetsUniqueConstraintMigrationCommand, + MigrateTimelineActivityToMorphRelationsCommand, ], exports: [ CleanEmptyStringNullInTextFieldsCommand, @@ -61,6 +75,7 @@ import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache RenameIndexNameCommand, MigrateStandardInvalidEntitiesCommand, UpdateRoleTargetsUniqueConstraintMigrationCommand, + MigrateTimelineActivityToMorphRelationsCommand, ], }) export class V1_13_UpgradeVersionCommandModule {} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts index d2c848d3da8..b099c1ac683 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts @@ -30,6 +30,7 @@ import { WorkspaceCustomApplicationIdNonNullableCommand } from 'src/database/com import { BackfillPageLayoutUniversalIdentifiersCommand } from 'src/database/commands/upgrade-version-command/1-13/1-13-backfill-page-layout-universal-identifiers.command'; import { DeduplicateRoleTargetsCommand } from 'src/database/commands/upgrade-version-command/1-13/1-13-deduplicate-role-targets.command'; import { MigrateStandardInvalidEntitiesCommand } from 'src/database/commands/upgrade-version-command/1-13/1-13-migrate-standard-invalid-entities.command'; +import { MigrateTimelineActivityToMorphRelationsCommand } from 'src/database/commands/upgrade-version-command/1-13/1-13-migrate-timeline-activity-to-morph-relations.command'; import { UpdateRoleTargetsUniqueConstraintMigrationCommand } from 'src/database/commands/upgrade-version-command/1-13/1-13-update-role-targets-unique-constraint-migration.command'; import { FixLabelIdentifierPositionAndVisibilityCommand } from 'src/database/commands/upgrade-version-command/1-6/1-6-fix-label-identifier-position-and-visibility.command'; import { BackfillWorkflowManualTriggerAvailabilityCommand } from 'src/database/commands/upgrade-version-command/1-7/1-7-backfill-workflow-manual-trigger-availability.command'; @@ -98,6 +99,7 @@ export class UpgradeCommand extends UpgradeCommandRunner { protected readonly cleanNullEquivalentValuesCommand: CleanNullEquivalentValuesCommand, // 1.13 Commands + protected readonly migrateTimelineActivityToMorphRelationsCommand: MigrateTimelineActivityToMorphRelationsCommand, protected readonly deduplicateRoleTargetsCommand: DeduplicateRoleTargetsCommand, protected readonly updateRoleTargetsUniqueConstraintMigrationCommand: UpdateRoleTargetsUniqueConstraintMigrationCommand, protected readonly backfillPageLayoutUniversalIdentifiersCommand: BackfillPageLayoutUniversalIdentifiersCommand, @@ -173,6 +175,7 @@ export class UpgradeCommand extends UpgradeCommandRunner { beforeSyncMetadata: [ this.deduplicateRoleTargetsCommand, this.updateRoleTargetsUniqueConstraintMigrationCommand, + this.migrateTimelineActivityToMorphRelationsCommand, this.backfillPageLayoutUniversalIdentifiersCommand, this.migrateStandardInvalidEntitiesCommand, ], diff --git a/packages/twenty-server/src/engine/core-modules/feature-flag/enums/feature-flag-key.enum.ts b/packages/twenty-server/src/engine/core-modules/feature-flag/enums/feature-flag-key.enum.ts index 366f7ad3bbf..515de1309fb 100644 --- a/packages/twenty-server/src/engine/core-modules/feature-flag/enums/feature-flag-key.enum.ts +++ b/packages/twenty-server/src/engine/core-modules/feature-flag/enums/feature-flag-key.enum.ts @@ -13,5 +13,6 @@ export enum FeatureFlagKey { IS_EMAILING_DOMAIN_ENABLED = 'IS_EMAILING_DOMAIN_ENABLED', IS_WORKFLOW_RUN_STOPPAGE_ENABLED = 'IS_WORKFLOW_RUN_STOPPAGE_ENABLED', IS_DASHBOARD_V2_ENABLED = 'IS_DASHBOARD_V2_ENABLED', + IS_TIMELINE_ACTIVITY_MIGRATED = 'IS_TIMELINE_ACTIVITY_MIGRATED', IS_GLOBAL_WORKSPACE_DATASOURCE_ENABLED = 'IS_GLOBAL_WORKSPACE_DATASOURCE_ENABLED', } diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/__tests__/__snapshots__/morph-relation-from-create-field-input-to-flat-field-metadatas-to-create.spec.ts.snap b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/__tests__/__snapshots__/morph-relation-from-create-field-input-to-flat-field-metadatas-to-create.spec.ts.snap index 61015dc7f2d..d3b68055080 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/__tests__/__snapshots__/morph-relation-from-create-field-input-to-flat-field-metadatas-to-create.spec.ts.snap +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/__tests__/__snapshots__/morph-relation-from-create-field-input-to-flat-field-metadatas-to-create.spec.ts.snap @@ -129,7 +129,7 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su "isNullable": true, "isSystem": false, "isUIReadOnly": false, - "isUnique": null, + "isUnique": false, "kanbanAggregateOperationViewIds": [], "label": "newFieldLabel", "mainGroupByFieldMetadataViewIds": [], @@ -156,7 +156,7 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su "calendarViewIds": [], "createdAt": Any, "defaultValue": null, - "description": null, + "description": "new field description", "icon": "IconPet", "id": Any, "isActive": true, @@ -165,7 +165,7 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su "isNullable": true, "isSystem": false, "isUIReadOnly": false, - "isUnique": null, + "isUnique": false, "kanbanAggregateOperationViewIds": [], "label": "Pet", "mainGroupByFieldMetadataViewIds": [], @@ -203,7 +203,7 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su "isNullable": true, "isSystem": false, "isUIReadOnly": false, - "isUnique": null, + "isUnique": false, "kanbanAggregateOperationViewIds": [], "label": "newFieldLabel", "mainGroupByFieldMetadataViewIds": [], @@ -230,7 +230,7 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su "calendarViewIds": [], "createdAt": Any, "defaultValue": null, - "description": null, + "description": "new field description", "icon": "IconBuilding", "id": Any, "isActive": true, @@ -239,7 +239,7 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su "isNullable": true, "isSystem": false, "isUIReadOnly": false, - "isUnique": null, + "isUnique": false, "kanbanAggregateOperationViewIds": [], "label": "Company", "mainGroupByFieldMetadataViewIds": [], diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/__tests__/generate-morph-or-relation-flat-field-metadata-pair.spec.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/__tests__/generate-morph-or-relation-flat-field-metadata-pair.spec.ts new file mode 100644 index 00000000000..27fa8c8b2e7 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/__tests__/generate-morph-or-relation-flat-field-metadata-pair.spec.ts @@ -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); + } + }, + ); + }); +}); diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/compute-flat-field-to-update-from-morph-relation-update-payload.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/compute-flat-field-to-update-from-morph-relation-update-payload.util.ts index b30e23196a7..671f5331ccf 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/compute-flat-field-to-update-from-morph-relation-update-payload.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/compute-flat-field-to-update-from-morph-relation-update-payload.util.ts @@ -99,6 +99,7 @@ export const computeFlatFieldToUpdateFromMorphRelationUpdatePayload = ({ }, sourceFlatObjectMetadata: sourceObjectMetadata, targetFlatObjectMetadata: newTargetObjectMetadata, + targetFlatFieldMetadataType: FieldMetadataType.RELATION, workspaceId: fieldMetadataToUpdate.workspaceId, workspaceCustomApplicationId, sourceFlatObjectMetadataJoinColumnName: diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-morph-relation-create-field-input-to-flat-field-metadatas.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-morph-relation-create-field-input-to-flat-field-metadatas.util.ts index cff0fa08f99..a3a13014bad 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-morph-relation-create-field-input-to-flat-field-metadatas.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-morph-relation-create-field-input-to-flat-field-metadatas.util.ts @@ -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, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-relation-create-field-input-to-flat-field-metadatas.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-relation-create-field-input-to-flat-field-metadatas.util.ts index 7615c3d52b9..c245f9d8f52 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-relation-create-field-input-to-flat-field-metadatas.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-relation-create-field-input-to-flat-field-metadatas.util.ts @@ -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, }); diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/generate-morph-or-relation-flat-field-metadata-pair.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/generate-morph-or-relation-flat-field-metadata-pair.util.ts index fbec1508985..55eede4816a 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/generate-morph-or-relation-flat-field-metadata-pair.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/generate-morph-or-relation-flat-field-metadata-pair.util.ts @@ -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 & 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, '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 = - { - ...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: diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/get-default-flat-field-metadata-from-create-field-input.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/get-default-flat-field-metadata-from-create-field-input.util.ts index 2624085313f..51d24cecca4 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/get-default-flat-field-metadata-from-create-field-input.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/get-default-flat-field-metadata-from-create-field-input.util.ts @@ -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, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-morph-relation-flat-field-metadata.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-morph-relation-flat-field-metadata.util.ts index 4bb1695aaf0..6d65ac31a82 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-morph-relation-flat-field-metadata.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-morph-relation-flat-field-metadata.util.ts @@ -56,5 +56,13 @@ export const validateMorphRelationFlatFieldMetadata = ( }); } + if (!isDefined(flatFieldMetadataToValidate.morphId)) { + errors.push({ + code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT, + message: 'Morph relation field must have a morph id', + userFriendlyMessage: msg`Morph relation field must have a morph id`, + }); + } + return errors; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/from-create-object-input-to-flat-object-metadata-and-flat-field-metadatas-to-create.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/from-create-object-input-to-flat-object-metadata-and-flat-field-metadatas-to-create.util.ts index ef40f5e12c9..5f007418111 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/from-create-object-input-to-flat-object-metadata-and-flat-field-metadatas-to-create.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/from-create-object-input-to-flat-object-metadata-and-flat-field-metadatas-to-create.util.ts @@ -4,6 +4,8 @@ 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 AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type'; import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type'; @@ -18,6 +20,7 @@ type FromCreateObjectInputToFlatObjectMetadataAndFlatFieldMetadatasToCreateArgs createObjectInput: CreateObjectInput; workspaceId: string; workspaceCustomApplicationId: string; + existingFeatureFlagsMap: FeatureFlagMap; } & Pick; export const fromCreateObjectInputToFlatObjectMetadataAndFlatFieldMetadatasToCreate = ({ @@ -25,6 +28,7 @@ export const fromCreateObjectInputToFlatObjectMetadataAndFlatFieldMetadatasToCre workspaceId, workspaceCustomApplicationId, flatObjectMetadataMaps: existingFlatObjectMetadataMaps, + existingFeatureFlagsMap, }: FromCreateObjectInputToFlatObjectMetadataAndFlatFieldMetadatasToCreateArgs): { flatObjectMetadataToCreate: FlatObjectMetadata; relationTargetFlatFieldMetadataToCreate: FlatFieldMetadata[]; @@ -96,6 +100,8 @@ export const fromCreateObjectInputToFlatObjectMetadataAndFlatFieldMetadatasToCre existingFlatObjectMetadataMaps, sourceFlatObjectMetadata: flatObjectMetadataToCreate, workspaceId, + workspaceCustomApplicationId, + existingFeatureFlagsMap, }); const objectFlatFieldMetadatas: FlatFieldMetadata[] = [ diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts index 830b1b69008..4cf72bf3132 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts @@ -324,13 +324,13 @@ export class ObjectMetadataService extends TypeOrmQueryService & { - sourceFlatObjectMetadata: FlatObjectMetadata; - targetFlatObjectMetadata: FlatObjectMetadata; -}): FlatFieldMetadata => { - 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; -}): FlatFieldMetadata => { - 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; 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, objectMetadataNameSingular) => { - const targetFlatObjectMetadataId = - objectIdByNameSingular[objectMetadataNameSingular]; + const result = + DEFAULT_RELATIONS_OBJECTS_STANDARD_IDS.reduce( + (sourceAndTargetFlatFieldMetadatasRecord, objectMetadataNameSingular) => { + const isObjectMigratedFromOlderReleases = + DEFAULT_MORPH_RELATIONS_OBJECTS_STANDARD_IDS.map(toString).includes( + objectMetadataNameSingular, + ); + const isFeatureFlagEnabled = + (objectMetadataNameSingular === 'timelineActivity' && + 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; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-table-relations/remote-table-relations.service.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-table-relations/remote-table-relations.service.ts index 56739f226ee..4697d002bfb 100644 --- a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-table-relations/remote-table-relations.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-table-relations/remote-table-relations.service.ts @@ -2,11 +2,11 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { - type FieldMetadataType, type FieldMetadataSettings, + type FieldMetadataType, } from 'twenty-shared/types'; -import { In, Repository } from 'typeorm'; import { isDefined } from 'twenty-shared/utils'; +import { In, Repository } from 'typeorm'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; @@ -210,7 +210,7 @@ export class RemoteTableRelationsService { { standardId: createForeignKeyDeterministicUuid({ objectId: createdObjectMetadata.id, - standardId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.custom, + standardId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.targetCustom, }), objectMetadataId: timelineActivityObjectMetadata.id, workspaceId: workspaceId, diff --git a/packages/twenty-server/src/engine/twenty-orm/custom.workspace-entity.ts b/packages/twenty-server/src/engine/twenty-orm/custom.workspace-entity.ts index 5f455424465..1a6737506ea 100644 --- a/packages/twenty-server/src/engine/twenty-orm/custom.workspace-entity.ts +++ b/packages/twenty-server/src/engine/twenty-orm/custom.workspace-entity.ts @@ -146,6 +146,7 @@ export class CustomWorkspaceEntity extends BaseWorkspaceEntity { }, icon: 'IconIconTimelineEvent', inverseSideTarget: () => TimelineActivityWorkspaceEntity, + inverseSideFieldKey: 'targetCustom', onDelete: RelationOnDeleteAction.CASCADE, }) @WorkspaceIsNullable() diff --git a/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts b/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts index 79b9fa49f3e..9028bafd0f8 100644 --- a/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts +++ b/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts @@ -205,6 +205,7 @@ describe('WorkspaceEntityManager', () => { IS_EMAILING_DOMAIN_ENABLED: false, IS_WORKFLOW_RUN_STOPPAGE_ENABLED: false, IS_DASHBOARD_V2_ENABLED: false, + IS_TIMELINE_ACTIVITY_MIGRATED: false, IS_GLOBAL_WORKSPACE_DATASOURCE_ENABLED: false, }, eventEmitterService: { diff --git a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-feature-flags.util.ts b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-feature-flags.util.ts index 8afbd6dc48b..56ef667cc30 100644 --- a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-feature-flags.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-feature-flags.util.ts @@ -81,6 +81,11 @@ export const seedFeatureFlags = async ({ workspaceId: workspaceId, value: true, }, + { + key: FeatureFlagKey.IS_TIMELINE_ACTIVITY_MIGRATED, + workspaceId: workspaceId, + value: true, + }, { key: FeatureFlagKey.IS_GLOBAL_WORKSPACE_DATASOURCE_ENABLED, workspaceId: workspaceId, diff --git a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/services/timeline-activity-seeder.service.ts b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/services/timeline-activity-seeder.service.ts index 4d3a297287e..f7f87645f36 100644 --- a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/services/timeline-activity-seeder.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/services/timeline-activity-seeder.service.ts @@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common'; import chunk from 'lodash.chunk'; import { ObjectRecord } from 'twenty-shared/types'; +import { capitalize } from 'twenty-shared/utils'; import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service'; import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager'; @@ -27,6 +28,7 @@ import { TASK_DATA_SEEDS } from 'src/engine/workspace-manager/dev-seeder/data/co import { TASK_TARGET_DATA_SEEDS_MAP } from 'src/engine/workspace-manager/dev-seeder/data/constants/task-target-data-seeds.constant'; import { WORKSPACE_MEMBER_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/workspace-member-data-seeds.constant'; import { type TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-objects/timeline-activity.workspace-entity'; +import { buildTimelineActivityRelatedMorphFieldMetadataName } from 'src/modules/timeline/utils/timeline-activity-related-morph-field-metadata-name-builder.util'; type RecordSeedWithId = Pick & Record; @@ -38,11 +40,11 @@ type TimelineActivitySeedData = Pick< | 'linkedRecordId' | 'linkedObjectMetadataId' | 'workspaceMemberId' - | 'companyId' - | 'personId' - | 'noteId' - | 'taskId' - | 'opportunityId' + | 'targetNoteId' + | 'targetTaskId' + | 'targetPersonId' + | 'targetCompanyId' + | 'targetOpportunityId' > & { properties: string; // JSON stringified for raw insertion createdAt: string; // ISO string for raw insertion @@ -253,11 +255,11 @@ export class TimelineActivitySeederService { 'linkedRecordId', 'linkedObjectMetadataId', 'workspaceMemberId', - 'companyId', - 'personId', - 'noteId', - 'taskId', - 'opportunityId', + 'targetNoteId', + 'targetTaskId', + 'targetPersonId', + 'targetCompanyId', + 'targetOpportunityId', 'createdAt', 'updatedAt', 'happensAt', @@ -290,21 +292,31 @@ export class TimelineActivitySeederService { linkedRecordId: recordId, linkedObjectMetadataId: null, workspaceMemberId: WORKSPACE_MEMBER_DATA_SEED_IDS.TIM, - companyId: null, - personId: null, - noteId: null, - taskId: null, - opportunityId: null, + targetNoteId: null, + targetTaskId: null, + targetPersonId: null, + targetCompanyId: null, + targetOpportunityId: null, createdAt: creationDate, updatedAt: creationDate, happensAt: creationDate, }; - // Set the appropriate entity ID - const entityIdKey = `${entityType}Id`; + // Set the appropriate target entity ID for entities that have target columns + const entitiesWithTargetColumns = new Set([ + 'note', + 'task', + 'person', + 'company', + 'opportunity', + ]); - // @ts-expect-error - This is okay for morph - timelineActivity[entityIdKey] = recordId; + if (entitiesWithTargetColumns.has(entityType)) { + const targetIdKey = `${buildTimelineActivityRelatedMorphFieldMetadataName(entityType)}Id`; + + // @ts-expect-error - This is okay for morph + timelineActivity[targetIdKey] = recordId; + } return timelineActivity; } @@ -576,30 +588,30 @@ export class TimelineActivitySeederService { linkedRecordId: recordSeed.id, linkedObjectMetadataId, workspaceMemberId: WORKSPACE_MEMBER_DATA_SEED_IDS.TIM, - companyId: null, - personId: null, - noteId: null, - taskId: null, - opportunityId: null, + targetNoteId: null, + targetTaskId: null, + targetPersonId: null, + targetCompanyId: null, + targetOpportunityId: null, createdAt: creationDate, updatedAt: creationDate, happensAt: creationDate, }; // Set target ID (person, company, or opportunity) - const targetIdKey = `${targetInfo.targetType}Id`; + const targetIdKey = `target${capitalize(targetInfo.targetType)}Id`; // @ts-expect-error - This is okay for morph linkedActivity[targetIdKey] = targetInfo.targetId; - // Only set activity ID for entities that have corresponding columns - const entitiesWithColumns = new Set(['note', 'task']); + // Only set target activity ID for entities that have corresponding columns + const entitiesWithTargetColumns = new Set(['note', 'task']); - if (entitiesWithColumns.has(activityType)) { - const activityIdKey = `${activityType}Id`; + if (entitiesWithTargetColumns.has(activityType)) { + const targetActivityIdKey = `target${capitalize(activityType)}Id`; // @ts-expect-error - This is okay for morph - linkedActivity[activityIdKey] = recordSeed.id; + linkedActivity[targetActivityIdKey] = recordSeed.id; } return linkedActivity; diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/constants/standard-object.constant.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/constants/standard-object.constant.ts index 58c9511c6a5..51bc92351d2 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/constants/standard-object.constant.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/constants/standard-object.constant.ts @@ -1555,32 +1555,40 @@ export const STANDARD_OBJECTS = { universalIdentifier: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.workspaceMember, }, - person: { - universalIdentifier: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.person, + targetPerson: { + universalIdentifier: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.targetPerson, }, - company: { - universalIdentifier: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.company, + targetCompany: { + universalIdentifier: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.targetCompany, }, - opportunity: { - universalIdentifier: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.opportunity, - }, - task: { universalIdentifier: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.task }, - note: { universalIdentifier: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.note }, - workflow: { - universalIdentifier: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.workflow, - }, - workflowVersion: { + targetOpportunity: { universalIdentifier: - TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.workflowVersion, + TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.targetOpportunity, }, - workflowRun: { - universalIdentifier: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.workflowRun, + targetTask: { + universalIdentifier: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.targetTask, }, - dashboard: { - universalIdentifier: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.dashboard, + targetNote: { + universalIdentifier: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.targetNote, }, - custom: { - universalIdentifier: '20202020-a9b2-4f85-b3c7-6d8e9f1a4c2b', + targetWorkflow: { + universalIdentifier: + TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.targetWorkflow, + }, + targetWorkflowVersion: { + universalIdentifier: + TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.targetWorkflowVersion, + }, + targetWorkflowRun: { + universalIdentifier: + TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.targetWorkflowRun, + }, + targetDashboard: { + universalIdentifier: + TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.targetDashboard, + }, + targetCustom: { + universalIdentifier: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.targetCustom, }, linkedRecordCachedName: { universalIdentifier: diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-company-standard-flat-field-metadata.util.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-company-standard-flat-field-metadata.util.ts index 9e76d96c86c..45264754dd8 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-company-standard-flat-field-metadata.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-company-standard-flat-field-metadata.util.ts @@ -472,7 +472,7 @@ export const buildCompanyStandardFlatFieldMetadatas = ({ isSystem: true, isNullable: true, targetObjectName: 'timelineActivity', - targetFieldName: 'company', + targetFieldName: 'targetCompany', settings: { relationType: RelationType.ONE_TO_MANY, }, diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-dashboard-standard-flat-field-metadata.util.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-dashboard-standard-flat-field-metadata.util.ts index 39285cd4df4..0139921a7d8 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-dashboard-standard-flat-field-metadata.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-dashboard-standard-flat-field-metadata.util.ts @@ -195,7 +195,7 @@ export const buildDashboardStandardFlatFieldMetadatas = ({ isSystem: true, isNullable: true, targetObjectName: 'timelineActivity', - targetFieldName: 'dashboard', + targetFieldName: 'targetDashboard', settings: { relationType: RelationType.ONE_TO_MANY, }, diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-note-standard-flat-field-metadata.util.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-note-standard-flat-field-metadata.util.ts index 7e4a59b8073..ebe5dc9d33b 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-note-standard-flat-field-metadata.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-note-standard-flat-field-metadata.util.ts @@ -258,7 +258,7 @@ export const buildNoteStandardFlatFieldMetadatas = ({ isSystem: true, isNullable: true, targetObjectName: 'timelineActivity', - targetFieldName: 'note', + targetFieldName: 'targetNote', settings: { relationType: RelationType.ONE_TO_MANY, }, diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-opportunity-standard-flat-field-metadata.util.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-opportunity-standard-flat-field-metadata.util.ts index ff9694287cd..b51848bc9f9 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-opportunity-standard-flat-field-metadata.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-opportunity-standard-flat-field-metadata.util.ts @@ -382,7 +382,7 @@ export const buildOpportunityStandardFlatFieldMetadatas = ({ isSystem: true, isNullable: true, targetObjectName: 'timelineActivity', - targetFieldName: 'opportunity', + targetFieldName: 'targetOpportunity', settings: { relationType: RelationType.ONE_TO_MANY, }, diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-person-standard-flat-field-metadata.util.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-person-standard-flat-field-metadata.util.ts index 6c851a31795..8c30528ea0b 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-person-standard-flat-field-metadata.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-person-standard-flat-field-metadata.util.ts @@ -492,7 +492,7 @@ export const buildPersonStandardFlatFieldMetadatas = ({ isSystem: true, isNullable: true, targetObjectName: 'timelineActivity', - targetFieldName: 'person', + targetFieldName: 'targetPerson', settings: { relationType: RelationType.ONE_TO_MANY, }, diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-task-standard-flat-field-metadata.util.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-task-standard-flat-field-metadata.util.ts index 694c6e2095b..6287aa8f550 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-task-standard-flat-field-metadata.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-task-standard-flat-field-metadata.util.ts @@ -327,7 +327,7 @@ export const buildTaskStandardFlatFieldMetadatas = ({ isSystem: true, isNullable: true, targetObjectName: 'timelineActivity', - targetFieldName: 'task', + targetFieldName: 'targetTask', settings: { relationType: RelationType.ONE_TO_MANY, }, diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-timeline-activity-standard-flat-field-metadata.util.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-timeline-activity-standard-flat-field-metadata.util.ts index 3d3f55cd223..fa1eb418a9a 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-timeline-activity-standard-flat-field-metadata.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-timeline-activity-standard-flat-field-metadata.util.ts @@ -7,10 +7,11 @@ import { import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; import { type AllStandardObjectFieldName } from 'src/engine/workspace-manager/twenty-standard-application/types/all-standard-object-field-name.type'; import { - type CreateStandardFieldArgs, createStandardFieldFlatMetadata, + type CreateStandardFieldArgs, } from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/create-standard-field-flat-metadata.util'; import { createStandardRelationFieldFlatMetadata } from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/create-standard-relation-field-flat-metadata.util'; +import { createStandardMorphRelationFieldFlatMetadata } from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/create-standard-morph-relation-field-flat-metadata.util'; export const buildTimelineActivityStandardFlatFieldMetadatas = ({ now, @@ -222,11 +223,11 @@ export const buildTimelineActivityStandardFlatFieldMetadatas = ({ twentyStandardApplicationId, now, }), - person: createStandardRelationFieldFlatMetadata({ + targetPerson: createStandardMorphRelationFieldFlatMetadata({ objectName, workspaceId, context: { - fieldName: 'person', + fieldName: 'targetPerson', label: 'Person', description: 'Event person', icon: 'IconUser', @@ -236,7 +237,7 @@ export const buildTimelineActivityStandardFlatFieldMetadatas = ({ settings: { relationType: RelationType.MANY_TO_ONE, onDelete: RelationOnDeleteAction.CASCADE, - joinColumnName: 'personId', + joinColumnName: 'targetPersonId', }, }, standardObjectMetadataRelatedEntityIds, @@ -244,11 +245,11 @@ export const buildTimelineActivityStandardFlatFieldMetadatas = ({ twentyStandardApplicationId, now, }), - company: createStandardRelationFieldFlatMetadata({ + targetCompany: createStandardMorphRelationFieldFlatMetadata({ objectName, workspaceId, context: { - fieldName: 'company', + fieldName: 'targetCompany', label: 'Company', description: 'Event company', icon: 'IconBuildingSkyscraper', @@ -258,7 +259,7 @@ export const buildTimelineActivityStandardFlatFieldMetadatas = ({ settings: { relationType: RelationType.MANY_TO_ONE, onDelete: RelationOnDeleteAction.CASCADE, - joinColumnName: 'companyId', + joinColumnName: 'targetCompanyId', }, }, standardObjectMetadataRelatedEntityIds, @@ -266,11 +267,11 @@ export const buildTimelineActivityStandardFlatFieldMetadatas = ({ twentyStandardApplicationId, now, }), - opportunity: createStandardRelationFieldFlatMetadata({ + targetOpportunity: createStandardMorphRelationFieldFlatMetadata({ objectName, workspaceId, context: { - fieldName: 'opportunity', + fieldName: 'targetOpportunity', label: 'Opportunity', description: 'Event opportunity', icon: 'IconTargetArrow', @@ -280,7 +281,7 @@ export const buildTimelineActivityStandardFlatFieldMetadatas = ({ settings: { relationType: RelationType.MANY_TO_ONE, onDelete: RelationOnDeleteAction.SET_NULL, - joinColumnName: 'opportunityId', + joinColumnName: 'targetOpportunityId', }, }, standardObjectMetadataRelatedEntityIds, @@ -288,11 +289,11 @@ export const buildTimelineActivityStandardFlatFieldMetadatas = ({ twentyStandardApplicationId, now, }), - note: createStandardRelationFieldFlatMetadata({ + targetNote: createStandardMorphRelationFieldFlatMetadata({ objectName, workspaceId, context: { - fieldName: 'note', + fieldName: 'targetNote', label: 'Note', description: 'Event note', icon: 'IconTargetArrow', @@ -302,7 +303,7 @@ export const buildTimelineActivityStandardFlatFieldMetadatas = ({ settings: { relationType: RelationType.MANY_TO_ONE, onDelete: RelationOnDeleteAction.SET_NULL, - joinColumnName: 'noteId', + joinColumnName: 'targetNoteId', }, }, standardObjectMetadataRelatedEntityIds, @@ -310,11 +311,11 @@ export const buildTimelineActivityStandardFlatFieldMetadatas = ({ twentyStandardApplicationId, now, }), - task: createStandardRelationFieldFlatMetadata({ + targetTask: createStandardMorphRelationFieldFlatMetadata({ objectName, workspaceId, context: { - fieldName: 'task', + fieldName: 'targetTask', label: 'Task', description: 'Event task', icon: 'IconTargetArrow', @@ -324,7 +325,7 @@ export const buildTimelineActivityStandardFlatFieldMetadatas = ({ settings: { relationType: RelationType.MANY_TO_ONE, onDelete: RelationOnDeleteAction.SET_NULL, - joinColumnName: 'taskId', + joinColumnName: 'targetTaskId', }, }, standardObjectMetadataRelatedEntityIds, @@ -332,11 +333,11 @@ export const buildTimelineActivityStandardFlatFieldMetadatas = ({ twentyStandardApplicationId, now, }), - workflow: createStandardRelationFieldFlatMetadata({ + targetWorkflow: createStandardMorphRelationFieldFlatMetadata({ objectName, workspaceId, context: { - fieldName: 'workflow', + fieldName: 'targetWorkflow', label: 'Workflow', description: 'Event workflow', icon: 'IconTargetArrow', @@ -346,7 +347,7 @@ export const buildTimelineActivityStandardFlatFieldMetadatas = ({ settings: { relationType: RelationType.MANY_TO_ONE, onDelete: RelationOnDeleteAction.CASCADE, - joinColumnName: 'workflowId', + joinColumnName: 'targetWorkflowId', }, }, standardObjectMetadataRelatedEntityIds, @@ -354,11 +355,11 @@ export const buildTimelineActivityStandardFlatFieldMetadatas = ({ twentyStandardApplicationId, now, }), - workflowVersion: createStandardRelationFieldFlatMetadata({ + targetWorkflowVersion: createStandardMorphRelationFieldFlatMetadata({ objectName, workspaceId, context: { - fieldName: 'workflowVersion', + fieldName: 'targetWorkflowVersion', label: 'WorkflowVersion', description: 'Event workflow version', icon: 'IconTargetArrow', @@ -368,7 +369,7 @@ export const buildTimelineActivityStandardFlatFieldMetadatas = ({ settings: { relationType: RelationType.MANY_TO_ONE, onDelete: RelationOnDeleteAction.CASCADE, - joinColumnName: 'workflowVersionId', + joinColumnName: 'targetWorkflowVersionId', }, }, standardObjectMetadataRelatedEntityIds, @@ -376,11 +377,11 @@ export const buildTimelineActivityStandardFlatFieldMetadatas = ({ twentyStandardApplicationId, now, }), - workflowRun: createStandardRelationFieldFlatMetadata({ + targetWorkflowRun: createStandardMorphRelationFieldFlatMetadata({ objectName, workspaceId, context: { - fieldName: 'workflowRun', + fieldName: 'targetWorkflowRun', label: 'Workflow Run', description: 'Event workflow run', icon: 'IconTargetArrow', @@ -390,7 +391,7 @@ export const buildTimelineActivityStandardFlatFieldMetadatas = ({ settings: { relationType: RelationType.MANY_TO_ONE, onDelete: RelationOnDeleteAction.CASCADE, - joinColumnName: 'workflowRunId', + joinColumnName: 'targetWorkflowRunId', }, }, standardObjectMetadataRelatedEntityIds, @@ -398,11 +399,11 @@ export const buildTimelineActivityStandardFlatFieldMetadatas = ({ twentyStandardApplicationId, now, }), - dashboard: createStandardRelationFieldFlatMetadata({ + targetDashboard: createStandardMorphRelationFieldFlatMetadata({ objectName, workspaceId, context: { - fieldName: 'dashboard', + fieldName: 'targetDashboard', label: 'Dashboard', description: 'Event dashboard', icon: 'IconTargetArrow', @@ -412,7 +413,7 @@ export const buildTimelineActivityStandardFlatFieldMetadatas = ({ settings: { relationType: RelationType.MANY_TO_ONE, onDelete: RelationOnDeleteAction.SET_NULL, - joinColumnName: 'dashboardId', + joinColumnName: 'targetDashboardId', }, }, standardObjectMetadataRelatedEntityIds, @@ -420,11 +421,11 @@ export const buildTimelineActivityStandardFlatFieldMetadatas = ({ twentyStandardApplicationId, now, }), - custom: createStandardRelationFieldFlatMetadata({ + targetCustom: createStandardMorphRelationFieldFlatMetadata({ objectName, workspaceId, context: { - fieldName: 'custom', + fieldName: 'targetCustom', label: 'Custom', description: 'Timeline Activity custom object', icon: 'IconTimeline', @@ -434,7 +435,7 @@ export const buildTimelineActivityStandardFlatFieldMetadatas = ({ settings: { relationType: RelationType.MANY_TO_ONE, onDelete: RelationOnDeleteAction.CASCADE, - joinColumnName: 'customId', + joinColumnName: 'targetCustomId', }, }, standardObjectMetadataRelatedEntityIds, diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-workflow-run-standard-flat-field-metadata.util.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-workflow-run-standard-flat-field-metadata.util.ts index 3d45243753c..7d70f2c6494 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-workflow-run-standard-flat-field-metadata.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-workflow-run-standard-flat-field-metadata.util.ts @@ -368,7 +368,7 @@ export const buildWorkflowRunStandardFlatFieldMetadatas = ({ isSystem: true, isNullable: false, targetObjectName: 'timelineActivity', - targetFieldName: 'workflowRun', + targetFieldName: 'targetWorkflowRun', settings: { relationType: RelationType.ONE_TO_MANY, }, diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-workflow-standard-flat-field-metadata.util.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-workflow-standard-flat-field-metadata.util.ts index 01f1f381e93..307e794ef21 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-workflow-standard-flat-field-metadata.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-workflow-standard-flat-field-metadata.util.ts @@ -324,7 +324,7 @@ export const buildWorkflowStandardFlatFieldMetadatas = ({ isSystem: true, isNullable: false, targetObjectName: 'timelineActivity', - targetFieldName: 'workflow', + targetFieldName: 'targetWorkflow', settings: { relationType: RelationType.ONE_TO_MANY, }, diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-workflow-version-standard-flat-field-metadata.util.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-workflow-version-standard-flat-field-metadata.util.ts index 5d519ad1a43..54f0990b2a4 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-workflow-version-standard-flat-field-metadata.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-workflow-version-standard-flat-field-metadata.util.ts @@ -283,7 +283,7 @@ export const buildWorkflowVersionStandardFlatFieldMetadatas = ({ isSystem: true, isNullable: false, targetObjectName: 'timelineActivity', - targetFieldName: 'workflowVersion', + targetFieldName: 'targetWorkflowVersion', settings: { relationType: RelationType.ONE_TO_MANY, }, diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/create-standard-morph-relation-field-flat-metadata.util.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/create-standard-morph-relation-field-flat-metadata.util.ts new file mode 100644 index 00000000000..eb4bb14adf7 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/create-standard-morph-relation-field-flat-metadata.util.ts @@ -0,0 +1,41 @@ +import { FieldMetadataType } from 'twenty-shared/types'; + +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type AllStandardObjectName } from 'src/engine/workspace-manager/twenty-standard-application/types/all-standard-object-name.type'; +import { type StandardBuilderArgs } from 'src/engine/workspace-manager/twenty-standard-application/types/metadata-standard-buillder-args.type'; +import { + type CreateStandardMorphOrRelationFieldContext, + createStandardRelationFieldFlatMetadata, +} from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/create-standard-relation-field-flat-metadata.util'; + +export type CreateStandardMorphRelationFieldContext< + O extends AllStandardObjectName, + T extends AllStandardObjectName, +> = CreateStandardMorphOrRelationFieldContext< + O, + T, + FieldMetadataType.MORPH_RELATION +>; + +export type CreateStandardMorphRelationFieldArgs< + O extends AllStandardObjectName, + T extends AllStandardObjectName, +> = StandardBuilderArgs<'fieldMetadata'> & { + objectName: O; + context: CreateStandardMorphRelationFieldContext; +}; + +export const createStandardMorphRelationFieldFlatMetadata = < + O extends AllStandardObjectName, + T extends AllStandardObjectName, +>( + args: CreateStandardMorphRelationFieldArgs, +): FlatFieldMetadata => { + const simpleRelationFlatFieldMetadata = + createStandardRelationFieldFlatMetadata(args); + + return { + ...simpleRelationFlatFieldMetadata, + type: FieldMetadataType.MORPH_RELATION, + }; +}; diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/create-standard-relation-field-flat-metadata.util.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/create-standard-relation-field-flat-metadata.util.ts index dacc51a35f9..cd5940c6168 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/create-standard-relation-field-flat-metadata.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/create-standard-relation-field-flat-metadata.util.ts @@ -15,6 +15,12 @@ import { type StandardBuilderArgs } from 'src/engine/workspace-manager/twenty-st export type CreateStandardRelationFieldContext< O extends AllStandardObjectName, T extends AllStandardObjectName, +> = CreateStandardMorphOrRelationFieldContext; + +export type CreateStandardMorphOrRelationFieldContext< + O extends AllStandardObjectName, + T extends AllStandardObjectName, + F extends FieldMetadataType.RELATION | FieldMetadataType.MORPH_RELATION, > = { fieldName: AllStandardObjectFieldName; label: string; @@ -26,7 +32,7 @@ export type CreateStandardRelationFieldContext< isNullable?: boolean; isUIReadOnly?: boolean; defaultValue?: FieldMetadataDefaultValueForAnyType; - settings: FieldMetadataSettings; + settings: FieldMetadataSettings; options?: FieldMetadataDefaultOption[] | FieldMetadataComplexOption[] | null; }; diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/index/compute-timeline-activity-standard-flat-index-metadata.util.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/index/compute-timeline-activity-standard-flat-index-metadata.util.ts index fbd5c2189b5..4b851290955 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/index/compute-timeline-activity-standard-flat-index-metadata.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/index/compute-timeline-activity-standard-flat-index-metadata.util.ts @@ -33,7 +33,7 @@ export const buildTimelineActivityStandardFlatIndexMetadatas = ({ workspaceId, context: { indexName: 'personIdIndex', - relatedFieldNames: ['person'], + relatedFieldNames: ['targetPerson'], }, standardObjectMetadataRelatedEntityIds, dependencyFlatEntityMaps, @@ -45,7 +45,7 @@ export const buildTimelineActivityStandardFlatIndexMetadatas = ({ workspaceId, context: { indexName: 'companyIdIndex', - relatedFieldNames: ['company'], + relatedFieldNames: ['targetCompany'], }, standardObjectMetadataRelatedEntityIds, dependencyFlatEntityMaps, @@ -57,7 +57,7 @@ export const buildTimelineActivityStandardFlatIndexMetadatas = ({ workspaceId, context: { indexName: 'opportunityIdIndex', - relatedFieldNames: ['opportunity'], + relatedFieldNames: ['targetOpportunity'], }, standardObjectMetadataRelatedEntityIds, dependencyFlatEntityMaps, @@ -69,7 +69,7 @@ export const buildTimelineActivityStandardFlatIndexMetadatas = ({ workspaceId, context: { indexName: 'noteIdIndex', - relatedFieldNames: ['note'], + relatedFieldNames: ['targetNote'], }, standardObjectMetadataRelatedEntityIds, dependencyFlatEntityMaps, @@ -81,7 +81,7 @@ export const buildTimelineActivityStandardFlatIndexMetadatas = ({ workspaceId, context: { indexName: 'taskIdIndex', - relatedFieldNames: ['task'], + relatedFieldNames: ['targetTask'], }, standardObjectMetadataRelatedEntityIds, dependencyFlatEntityMaps, @@ -93,7 +93,7 @@ export const buildTimelineActivityStandardFlatIndexMetadatas = ({ workspaceId, context: { indexName: 'workflowIdIndex', - relatedFieldNames: ['workflow'], + relatedFieldNames: ['targetWorkflow'], }, standardObjectMetadataRelatedEntityIds, dependencyFlatEntityMaps, @@ -105,7 +105,7 @@ export const buildTimelineActivityStandardFlatIndexMetadatas = ({ workspaceId, context: { indexName: 'workflowVersionIdIndex', - relatedFieldNames: ['workflowVersion'], + relatedFieldNames: ['targetWorkflowVersion'], }, standardObjectMetadataRelatedEntityIds, dependencyFlatEntityMaps, @@ -117,7 +117,7 @@ export const buildTimelineActivityStandardFlatIndexMetadatas = ({ workspaceId, context: { indexName: 'workflowRunIdIndex', - relatedFieldNames: ['workflowRun'], + relatedFieldNames: ['targetWorkflowRun'], }, standardObjectMetadataRelatedEntityIds, dependencyFlatEntityMaps, @@ -129,7 +129,7 @@ export const buildTimelineActivityStandardFlatIndexMetadatas = ({ workspaceId, context: { indexName: 'dashboardIdIndex', - relatedFieldNames: ['dashboard'], + relatedFieldNames: ['targetDashboard'], }, standardObjectMetadataRelatedEntityIds, dependencyFlatEntityMaps, diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/default-feature-flags.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/default-feature-flags.ts index 57e35bf6696..ea92f74cd16 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/default-feature-flags.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/default-feature-flags.ts @@ -1,3 +1,5 @@ -import { type FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; +import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; -export const DEFAULT_FEATURE_FLAGS = [] as const satisfies FeatureFlagKey[]; +export const DEFAULT_FEATURE_FLAGS = [ + FeatureFlagKey.IS_TIMELINE_ACTIVITY_MIGRATED, +] as const satisfies FeatureFlagKey[]; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts index 12e5cf20c12..f8984753c83 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts @@ -163,19 +163,20 @@ export const TIMELINE_ACTIVITY_STANDARD_FIELD_IDS = { name: '20202020-7207-46e8-9dab-849505ae8497', properties: '20202020-f142-4b04-b91b-6a2b4af3bf11', workspaceMember: '20202020-af23-4479-9a30-868edc474b36', - person: '20202020-c414-45b9-a60a-ac27aa96229f', - company: '20202020-04ad-4221-a744-7a8278a5ce21', - opportunity: '20202020-7664-4a35-a3df-580d389fd527', - task: '20202020-b2f5-415c-9135-a31dfe49501b', - note: '20202020-ec55-4135-8da5-3a20badc0156', - workflow: '20202020-616c-4ad3-a2e9-c477c341e295', - workflowVersion: '20202020-74f1-4711-a129-e14ca0ecd744', - workflowRun: '20202020-96f0-401b-9186-a3a0759225ac', - dashboard: '20202020-7864-48f5-af7c-9e4b60140948', - custom: '20202020-4a71-41b0-9f83-9cdcca3f8b14', + targetPerson: '20202020-c414-45b9-a60a-ac27aa96229f', + targetCompany: '20202020-04ad-4221-a744-7a8278a5ce21', + targetOpportunity: '20202020-7664-4a35-a3df-580d389fd527', + targetTask: '20202020-b2f5-415c-9135-a31dfe49501b', + targetNote: '20202020-ec55-4135-8da5-3a20badc0156', + targetWorkflow: '20202020-616c-4ad3-a2e9-c477c341e295', + targetWorkflowVersion: '20202020-74f1-4711-a129-e14ca0ecd744', + targetWorkflowRun: '20202020-96f0-401b-9186-a3a0759225ac', + targetDashboard: '20202020-7864-48f5-af7c-9e4b60140948', + targetCustom: '20202020-4a71-41b0-9f83-9cdcca3f8b14', linkedRecordCachedName: '20202020-cfdb-4bef-bbce-a29f41230934', linkedRecordId: '20202020-2e0e-48c0-b445-ee6c1e61687d', linkedObjectMetadataId: '20202020-c595-449d-9f89-562758c9ee69', + targetMorphId: '20202020-9a2b-4c3d-a4e5-f6a7b8c9d0e1', } as const; export const FAVORITE_STANDARD_FIELD_IDS = { diff --git a/packages/twenty-server/src/modules/attachment/standard-objects/attachment.workspace-entity.ts b/packages/twenty-server/src/modules/attachment/standard-objects/attachment.workspace-entity.ts index 7eddc62e4b5..d52f3dc4a01 100644 --- a/packages/twenty-server/src/modules/attachment/standard-objects/attachment.workspace-entity.ts +++ b/packages/twenty-server/src/modules/attachment/standard-objects/attachment.workspace-entity.ts @@ -272,6 +272,7 @@ export class AttachmentWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceJoinColumn('workflow') workflowId: string | null; + // todo: remove this decorator and the custom field @WorkspaceDynamicRelation({ type: RelationType.MANY_TO_ONE, argsFactory: (oppositeObjectMetadata) => ({ diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-participant-manager/calendar-event-participant-manager.module.ts b/packages/twenty-server/src/modules/calendar/calendar-event-participant-manager/calendar-event-participant-manager.module.ts index 23ff7bf34d1..6192af96d12 100644 --- a/packages/twenty-server/src/modules/calendar/calendar-event-participant-manager/calendar-event-participant-manager.module.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-participant-manager/calendar-event-participant-manager.module.ts @@ -3,10 +3,16 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm'; +import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; +import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { WorkspaceFeatureFlagsMapCacheService } from 'src/engine/metadata-modules/workspace-feature-flags-map-cache/workspace-feature-flags-map-cache.service'; +import { GetDataFromCacheWithRecomputeService } from 'src/engine/workspace-cache-storage/services/get-data-from-cache-with-recompute.service'; +import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; +import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module'; import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module'; import { CalendarEventParticipantMatchParticipantJob } from 'src/modules/calendar/calendar-event-participant-manager/jobs/calendar-event-participant-match-participant.job'; import { CalendarEventParticipantPersonListener } from 'src/modules/calendar/calendar-event-participant-manager/listeners/calendar-event-participant-person.listener'; @@ -20,10 +26,15 @@ import { MatchParticipantModule } from 'src/modules/match-participant/match-part imports: [ WorkspaceDataSourceModule, WorkspaceModule, - TypeOrmModule.forFeature([ObjectMetadataEntity, FieldMetadataEntity]), + TypeOrmModule.forFeature([ + ObjectMetadataEntity, + FieldMetadataEntity, + FeatureFlagEntity, + ]), NestjsQueryTypeOrmModule.forFeature([WorkspaceEntity]), ContactCreationManagerModule, MatchParticipantModule, + WorkspaceCacheModule, ], providers: [ CalendarEventParticipantService, @@ -31,6 +42,10 @@ import { MatchParticipantModule } from 'src/modules/match-participant/match-part CalendarEventParticipantListener, CalendarEventParticipantPersonListener, CalendarEventParticipantWorkspaceMemberListener, + FeatureFlagService, + WorkspaceFeatureFlagsMapCacheService, + WorkspaceCacheStorageService, + GetDataFromCacheWithRecomputeService, ], exports: [CalendarEventParticipantService], }) diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-participant-manager/listeners/calendar-event-participant.listener.ts b/packages/twenty-server/src/modules/calendar/calendar-event-participant-manager/listeners/calendar-event-participant.listener.ts index ab0ff293643..60b605051d0 100644 --- a/packages/twenty-server/src/modules/calendar/calendar-event-participant-manager/listeners/calendar-event-participant.listener.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-participant-manager/listeners/calendar-event-participant.listener.ts @@ -5,12 +5,14 @@ import { isDefined } from 'twenty-shared/utils'; import { Repository } from 'typeorm'; import { OnCustomBatchEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-custom-batch-event.decorator'; +import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; +import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator'; +import { CustomWorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/custom-workspace-batch-event.type'; import { type CalendarEventParticipantWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event-participant.workspace-entity'; import { TimelineActivityRepository } from 'src/modules/timeline/repositories/timeline-activity.repository'; import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-objects/timeline-activity.workspace-entity'; -import { CustomWorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/custom-workspace-batch-event.type'; @Injectable() export class CalendarEventParticipantListener { @@ -19,6 +21,7 @@ export class CalendarEventParticipantListener { private readonly timelineActivityRepository: TimelineActivityRepository, @InjectRepository(ObjectMetadataEntity) private readonly objectMetadataRepository: Repository, + private readonly featureFlagService: FeatureFlagService, ) {} @OnCustomBatchEvent('calendarEventParticipant_matched') @@ -72,10 +75,17 @@ export class CalendarEventParticipantListener { .filter(isDefined); }); + const isFeatureFlagTimelineActivityMigrated = + await this.featureFlagService.isFeatureEnabled( + FeatureFlagKey.IS_TIMELINE_ACTIVITY_MIGRATED, + batchEvent.workspaceId, + ); + await this.timelineActivityRepository.upsertTimelineActivities({ objectSingularName: 'person', workspaceId: batchEvent.workspaceId, payloads: timelineActivityPayloads.filter(isDefined), + isFeatureFlagTimelineActivityMigrated, }); } } diff --git a/packages/twenty-server/src/modules/company/standard-objects/company.workspace-entity.ts b/packages/twenty-server/src/modules/company/standard-objects/company.workspace-entity.ts index efd9c010ad4..e90e4882c8e 100644 --- a/packages/twenty-server/src/modules/company/standard-objects/company.workspace-entity.ts +++ b/packages/twenty-server/src/modules/company/standard-objects/company.workspace-entity.ts @@ -266,6 +266,7 @@ export class CompanyWorkspaceEntity extends BaseWorkspaceEntity { description: msg`Timeline Activities linked to the company`, icon: 'IconIconTimelineEvent', inverseSideTarget: () => TimelineActivityWorkspaceEntity, + inverseSideFieldKey: 'targetCompany', onDelete: RelationOnDeleteAction.CASCADE, }) @WorkspaceIsNullable() diff --git a/packages/twenty-server/src/modules/dashboard/standard-objects/dashboard.workspace-entity.ts b/packages/twenty-server/src/modules/dashboard/standard-objects/dashboard.workspace-entity.ts index 3d8dde8b2c3..ce125e6e1b8 100644 --- a/packages/twenty-server/src/modules/dashboard/standard-objects/dashboard.workspace-entity.ts +++ b/packages/twenty-server/src/modules/dashboard/standard-objects/dashboard.workspace-entity.ts @@ -97,6 +97,7 @@ export class DashboardWorkspaceEntity extends BaseWorkspaceEntity { label: msg`Timeline Activities`, description: msg`Timeline activities linked to the dashboard`, inverseSideTarget: () => TimelineActivityWorkspaceEntity, + inverseSideFieldKey: 'targetDashboard', onDelete: RelationOnDeleteAction.SET_NULL, }) @WorkspaceIsNullable() diff --git a/packages/twenty-server/src/modules/favorite/standard-objects/favorite.workspace-entity.ts b/packages/twenty-server/src/modules/favorite/standard-objects/favorite.workspace-entity.ts index 7ccdda36274..5fe50593bbc 100644 --- a/packages/twenty-server/src/modules/favorite/standard-objects/favorite.workspace-entity.ts +++ b/packages/twenty-server/src/modules/favorite/standard-objects/favorite.workspace-entity.ts @@ -1,6 +1,6 @@ import { msg } from '@lingui/core/macro'; -import { FieldMetadataType, RelationOnDeleteAction } from 'twenty-shared/types'; import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata'; +import { FieldMetadataType, RelationOnDeleteAction } from 'twenty-shared/types'; import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; @@ -237,6 +237,7 @@ export class FavoriteWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceIsNullable() viewId: string; + // todo: remove this decorator and the custom field @WorkspaceDynamicRelation({ type: RelationType.MANY_TO_ONE, argsFactory: (oppositeObjectMetadata) => ({ diff --git a/packages/twenty-server/src/modules/messaging/message-participant-manager/listeners/message-participant.listener.ts b/packages/twenty-server/src/modules/messaging/message-participant-manager/listeners/message-participant.listener.ts index 85faea7a9ab..2da29730a5c 100644 --- a/packages/twenty-server/src/modules/messaging/message-participant-manager/listeners/message-participant.listener.ts +++ b/packages/twenty-server/src/modules/messaging/message-participant-manager/listeners/message-participant.listener.ts @@ -5,12 +5,14 @@ import { isDefined } from 'twenty-shared/utils'; import { Repository } from 'typeorm'; import { OnCustomBatchEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-custom-batch-event.decorator'; +import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; +import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator'; +import { CustomWorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/custom-workspace-batch-event.type'; import { type MessageParticipantWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-participant.workspace-entity'; import { TimelineActivityRepository } from 'src/modules/timeline/repositories/timeline-activity.repository'; import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-objects/timeline-activity.workspace-entity'; -import { CustomWorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/custom-workspace-batch-event.type'; @Injectable() export class MessageParticipantListener { @@ -19,6 +21,7 @@ export class MessageParticipantListener { private readonly timelineActivityRepository: TimelineActivityRepository, @InjectRepository(ObjectMetadataEntity) private readonly objectMetadataRepository: Repository, + private readonly featureFlagService: FeatureFlagService, ) {} @OnCustomBatchEvent('messageParticipant_matched') @@ -71,10 +74,17 @@ export class MessageParticipantListener { .filter(isDefined); }); + const isFeatureFlagTimelineActivityMigrated = + await this.featureFlagService.isFeatureEnabled( + FeatureFlagKey.IS_TIMELINE_ACTIVITY_MIGRATED, + batchEvent.workspaceId, + ); + await this.timelineActivityRepository.upsertTimelineActivities({ objectSingularName: 'person', workspaceId: batchEvent.workspaceId, payloads: timelineActivityPayloads.filter(isDefined), + isFeatureFlagTimelineActivityMigrated, }); } } diff --git a/packages/twenty-server/src/modules/messaging/message-participant-manager/message-participant-manager.module.ts b/packages/twenty-server/src/modules/messaging/message-participant-manager/message-participant-manager.module.ts index d999a32eb5d..d54092c0e2e 100644 --- a/packages/twenty-server/src/modules/messaging/message-participant-manager/message-participant-manager.module.ts +++ b/packages/twenty-server/src/modules/messaging/message-participant-manager/message-participant-manager.module.ts @@ -3,9 +3,16 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { AuditModule } from 'src/engine/core-modules/audit/audit.module'; import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; +import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity'; +import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; +import { WorkspaceFeatureFlagsMapCacheService } from 'src/engine/metadata-modules/workspace-feature-flags-map-cache/workspace-feature-flags-map-cache.service'; import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module'; +import { GetDataFromCacheWithRecomputeService } from 'src/engine/workspace-cache-storage/services/get-data-from-cache-with-recompute.service'; +import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; +import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module'; import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module'; import { ContactCreationManagerModule } from 'src/modules/contact-creation-manager/contact-creation-manager.module'; import { MatchParticipantModule } from 'src/modules/match-participant/match-participant.module'; @@ -23,6 +30,8 @@ import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-o FeatureFlagEntity, WorkspaceEntity, ObjectMetadataEntity, + RoleEntity, + RoleTargetEntity, ]), AuditModule, ContactCreationManagerModule, @@ -32,6 +41,7 @@ import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-o ]), MessagingCommonModule, MatchParticipantModule, + WorkspaceCacheModule, ], providers: [ MessagingMessageParticipantService, @@ -39,6 +49,10 @@ import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-o MessageParticipantListener, MessageParticipantPersonListener, MessageParticipantWorkspaceMemberListener, + FeatureFlagService, + WorkspaceFeatureFlagsMapCacheService, + WorkspaceCacheStorageService, + GetDataFromCacheWithRecomputeService, ], exports: [MessagingMessageParticipantService], }) diff --git a/packages/twenty-server/src/modules/note/standard-objects/note-target.workspace-entity.ts b/packages/twenty-server/src/modules/note/standard-objects/note-target.workspace-entity.ts index c7022bac85e..c66eaa82db6 100644 --- a/packages/twenty-server/src/modules/note/standard-objects/note-target.workspace-entity.ts +++ b/packages/twenty-server/src/modules/note/standard-objects/note-target.workspace-entity.ts @@ -1,6 +1,6 @@ import { msg } from '@lingui/core/macro'; -import { RelationOnDeleteAction } from 'twenty-shared/types'; import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata'; +import { RelationOnDeleteAction } from 'twenty-shared/types'; import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; @@ -95,6 +95,7 @@ export class NoteTargetWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceJoinColumn('opportunity') opportunityId: string | null; + // todo: remove this decorator and the custom field @WorkspaceDynamicRelation({ type: RelationType.MANY_TO_ONE, argsFactory: (oppositeObjectMetadata) => ({ diff --git a/packages/twenty-server/src/modules/note/standard-objects/note.workspace-entity.ts b/packages/twenty-server/src/modules/note/standard-objects/note.workspace-entity.ts index 709f20c5de7..f7ce539ce65 100644 --- a/packages/twenty-server/src/modules/note/standard-objects/note.workspace-entity.ts +++ b/packages/twenty-server/src/modules/note/standard-objects/note.workspace-entity.ts @@ -1,17 +1,17 @@ import { msg } from '@lingui/core/macro'; +import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata'; import { ActorMetadata, FieldMetadataType, RelationOnDeleteAction, type RichTextV2Metadata, } from 'twenty-shared/types'; -import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata'; import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; -import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/search-field-metadata/constants/search-vector-field.constants'; import { IndexType } from 'src/engine/metadata-modules/index-metadata/types/indexType.types'; +import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/search-field-metadata/constants/search-vector-field.constants'; import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator'; import { WorkspaceFieldIndex } from 'src/engine/twenty-orm/decorators/workspace-field-index.decorator'; @@ -127,6 +127,7 @@ export class NoteWorkspaceEntity extends BaseWorkspaceEntity { description: msg`Timeline Activities linked to the note.`, icon: 'IconTimelineEvent', inverseSideTarget: () => TimelineActivityWorkspaceEntity, + inverseSideFieldKey: 'targetNote', onDelete: RelationOnDeleteAction.SET_NULL, }) @WorkspaceIsNullable() diff --git a/packages/twenty-server/src/modules/opportunity/standard-objects/opportunity.workspace-entity.ts b/packages/twenty-server/src/modules/opportunity/standard-objects/opportunity.workspace-entity.ts index a03cc51dc4b..ed8a3741e2a 100644 --- a/packages/twenty-server/src/modules/opportunity/standard-objects/opportunity.workspace-entity.ts +++ b/packages/twenty-server/src/modules/opportunity/standard-objects/opportunity.workspace-entity.ts @@ -1,17 +1,17 @@ import { msg } from '@lingui/core/macro'; +import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata'; import { ActorMetadata, type CurrencyMetadata, FieldMetadataType, RelationOnDeleteAction, } from 'twenty-shared/types'; -import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata'; import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; -import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/search-field-metadata/constants/search-vector-field.constants'; import { IndexType } from 'src/engine/metadata-modules/index-metadata/types/indexType.types'; +import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/search-field-metadata/constants/search-vector-field.constants'; import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator'; import { WorkspaceFieldIndex } from 'src/engine/twenty-orm/decorators/workspace-field-index.decorator'; @@ -219,6 +219,7 @@ export class OpportunityWorkspaceEntity extends BaseWorkspaceEntity { description: msg`Timeline Activities linked to the opportunity.`, icon: 'IconTimelineEvent', inverseSideTarget: () => TimelineActivityWorkspaceEntity, + inverseSideFieldKey: 'targetOpportunity', onDelete: RelationOnDeleteAction.SET_NULL, }) @WorkspaceIsNullable() diff --git a/packages/twenty-server/src/modules/person/standard-objects/person.workspace-entity.ts b/packages/twenty-server/src/modules/person/standard-objects/person.workspace-entity.ts index f6c156307e0..582aa946d32 100644 --- a/packages/twenty-server/src/modules/person/standard-objects/person.workspace-entity.ts +++ b/packages/twenty-server/src/modules/person/standard-objects/person.workspace-entity.ts @@ -306,6 +306,7 @@ export class PersonWorkspaceEntity extends BaseWorkspaceEntity { description: msg`Events linked to the person`, icon: 'IconTimelineEvent', inverseSideTarget: () => TimelineActivityWorkspaceEntity, + inverseSideFieldKey: 'targetPerson', onDelete: RelationOnDeleteAction.CASCADE, }) @WorkspaceIsNullable() diff --git a/packages/twenty-server/src/modules/task/standard-objects/task-target.workspace-entity.ts b/packages/twenty-server/src/modules/task/standard-objects/task-target.workspace-entity.ts index 46b97e8047d..c6c7f602651 100644 --- a/packages/twenty-server/src/modules/task/standard-objects/task-target.workspace-entity.ts +++ b/packages/twenty-server/src/modules/task/standard-objects/task-target.workspace-entity.ts @@ -1,6 +1,6 @@ import { msg } from '@lingui/core/macro'; -import { RelationOnDeleteAction } from 'twenty-shared/types'; import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata'; +import { RelationOnDeleteAction } from 'twenty-shared/types'; import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; @@ -95,6 +95,7 @@ export class TaskTargetWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceJoinColumn('opportunity') opportunityId: string | null; + // todo: remove this decorator and the custom field @WorkspaceDynamicRelation({ type: RelationType.MANY_TO_ONE, argsFactory: (oppositeObjectMetadata) => ({ diff --git a/packages/twenty-server/src/modules/task/standard-objects/task.workspace-entity.ts b/packages/twenty-server/src/modules/task/standard-objects/task.workspace-entity.ts index 0fa9ea47533..f94e638fa8a 100644 --- a/packages/twenty-server/src/modules/task/standard-objects/task.workspace-entity.ts +++ b/packages/twenty-server/src/modules/task/standard-objects/task.workspace-entity.ts @@ -182,6 +182,7 @@ export class TaskWorkspaceEntity extends BaseWorkspaceEntity { description: msg`Timeline Activities linked to the task.`, icon: 'IconTimelineEvent', inverseSideTarget: () => TimelineActivityWorkspaceEntity, + inverseSideFieldKey: 'targetTask', onDelete: RelationOnDeleteAction.SET_NULL, }) @WorkspaceIsNullable() diff --git a/packages/twenty-server/src/modules/timeline/repositories/timeline-activity.repository.ts b/packages/twenty-server/src/modules/timeline/repositories/timeline-activity.repository.ts index a7bb64ca30d..eafaad9cb75 100644 --- a/packages/twenty-server/src/modules/timeline/repositories/timeline-activity.repository.ts +++ b/packages/twenty-server/src/modules/timeline/repositories/timeline-activity.repository.ts @@ -8,6 +8,7 @@ import { objectRecordDiffMerge } from 'src/engine/core-modules/event-emitter/uti 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 { type TimelineActivityPayload } from 'src/modules/timeline/types/timeline-activity-payload'; +import { buildTimelineActivityRelatedMorphFieldMetadataName } from 'src/modules/timeline/utils/timeline-activity-related-morph-field-metadata-name-builder.util'; type TimelineActivityPayloadWorkspaceIdAndObjectSingularName = { payloads: (Omit & { @@ -15,6 +16,7 @@ type TimelineActivityPayloadWorkspaceIdAndObjectSingularName = { })[]; workspaceId: string; objectSingularName: string; + isFeatureFlagTimelineActivityMigrated: boolean; }; @Injectable() @@ -27,6 +29,7 @@ export class TimelineActivityRepository { objectSingularName, workspaceId, payloads, + isFeatureFlagTimelineActivityMigrated, }: TimelineActivityPayloadWorkspaceIdAndObjectSingularName) { const authContext = buildSystemAuthContext(workspaceId); @@ -38,6 +41,7 @@ export class TimelineActivityRepository { objectSingularName, workspaceId, payloads, + isFeatureFlagTimelineActivityMigrated, }); const payloadsWithDiff = payloads @@ -59,10 +63,16 @@ export class TimelineActivityRepository { const payloadsToInsert: TimelineActivityPayloadWorkspaceIdAndObjectSingularName['payloads'] = []; + const timelineActivityPropertyName = + await this.getTimelineActivityPropertyName( + objectSingularName, + isFeatureFlagTimelineActivityMigrated, + ); + for (const payload of payloadsWithDiff) { const recentTimelineActivity = recentTimelineActivities.find( (timelineActivity) => - timelineActivity[`${objectSingularName}Id`] === + timelineActivity[timelineActivityPropertyName] === payload.recordId && timelineActivity.workspaceMemberId === payload.workspaceMemberId && @@ -92,6 +102,7 @@ export class TimelineActivityRepository { objectSingularName, payloads: payloadsToInsert, workspaceId, + isFeatureFlagTimelineActivityMigrated, }); }, ); @@ -101,6 +112,7 @@ export class TimelineActivityRepository { objectSingularName, workspaceId, payloads, + isFeatureFlagTimelineActivityMigrated, }: TimelineActivityPayloadWorkspaceIdAndObjectSingularName) { const timelineActivityTypeORMRepository = await this.globalWorkspaceOrmManager.getRepository( @@ -113,8 +125,14 @@ export class TimelineActivityRepository { const tenMinutesAgo = new Date(Date.now() - 10 * 60 * 1000); + const timelineActivityPropertyName = + await this.getTimelineActivityPropertyName( + objectSingularName, + isFeatureFlagTimelineActivityMigrated, + ); + const whereConditions: Record = { - [`${objectSingularName}Id`]: In( + [timelineActivityPropertyName]: In( payloads.map((payload) => payload.recordId), ), name: In(payloads.map((payload) => payload.name)), @@ -135,6 +153,7 @@ export class TimelineActivityRepository { objectSingularName, workspaceId, payloads, + isFeatureFlagTimelineActivityMigrated, }: TimelineActivityPayloadWorkspaceIdAndObjectSingularName) { if (payloads.length === 0) { return; @@ -149,12 +168,18 @@ export class TimelineActivityRepository { }, ); + const timelineActivityPropertyName = + await this.getTimelineActivityPropertyName( + objectSingularName, + isFeatureFlagTimelineActivityMigrated, + ); + return timelineActivityTypeORMRepository.insert( payloads.map((payload) => ({ name: payload.name, properties: payload.properties, workspaceMemberId: payload.workspaceMemberId, - [`${objectSingularName}Id`]: payload.recordId, + [timelineActivityPropertyName]: payload.recordId, linkedRecordCachedName: payload.linkedRecordCachedName ?? '', linkedRecordId: payload.linkedRecordId, linkedObjectMetadataId: payload.linkedObjectMetadataId, @@ -187,4 +212,13 @@ export class TimelineActivityRepository { workspaceMemberId: workspaceMemberId, }); } + + private async getTimelineActivityPropertyName( + objectSingularName: string, + isFeatureFlagTimelineActivityMigrated: boolean, + ) { + return isFeatureFlagTimelineActivityMigrated + ? `${buildTimelineActivityRelatedMorphFieldMetadataName(objectSingularName)}Id` + : `${objectSingularName}Id`; + } } diff --git a/packages/twenty-server/src/modules/timeline/services/timeline-activity.service.ts b/packages/twenty-server/src/modules/timeline/services/timeline-activity.service.ts index 41a25f52f6f..f097ae84eca 100644 --- a/packages/twenty-server/src/modules/timeline/services/timeline-activity.service.ts +++ b/packages/twenty-server/src/modules/timeline/services/timeline-activity.service.ts @@ -6,6 +6,8 @@ import { In } from 'typeorm'; import { getFlatFieldsFromFlatObjectMetadata } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-flat-fields-for-flat-object-metadata.util'; import { type ObjectRecordBaseEvent } from 'src/engine/core-modules/event-emitter/types/object-record.base.event'; +import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; +import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator'; import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; @@ -25,6 +27,7 @@ export class TimelineActivityService { constructor( @InjectObjectMetadataRepository(TimelineActivityWorkspaceEntity) private readonly timelineActivityRepository: TimelineActivityRepository, + private readonly featureFlagService: FeatureFlagService, private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, ) {} @@ -76,11 +79,18 @@ export class TimelineActivityService { {} as Record, ); + const isFeatureFlagTimelineActivityMigrated = + await this.featureFlagService.isFeatureEnabled( + FeatureFlagKey.IS_TIMELINE_ACTIVITY_MIGRATED, + workspaceId, + ); + for (const objectSingularName in payloadsByObjectSingularName) { - this.timelineActivityRepository.upsertTimelineActivities({ + await this.timelineActivityRepository.upsertTimelineActivities({ objectSingularName, workspaceId, payloads: payloadsByObjectSingularName[objectSingularName], + isFeatureFlagTimelineActivityMigrated, }); } } diff --git a/packages/twenty-server/src/modules/timeline/standard-objects/timeline-activity.workspace-entity.ts b/packages/twenty-server/src/modules/timeline/standard-objects/timeline-activity.workspace-entity.ts index 9b84bec7840..606d72bb3b0 100644 --- a/packages/twenty-server/src/modules/timeline/standard-objects/timeline-activity.workspace-entity.ts +++ b/packages/twenty-server/src/modules/timeline/standard-objects/timeline-activity.workspace-entity.ts @@ -1,6 +1,7 @@ import { msg } from '@lingui/core/macro'; import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata'; import { FieldMetadataType, RelationOnDeleteAction } from 'twenty-shared/types'; +import { capitalize } from 'twenty-shared/utils'; import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; @@ -120,7 +121,7 @@ export class TimelineActivityWorkspaceEntity extends BaseWorkspaceEntity { workspaceMemberId: string | null; @WorkspaceRelation({ - standardId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.person, + standardId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.targetPerson, type: RelationType.MANY_TO_ONE, label: msg`Person`, description: msg`Event person`, @@ -128,15 +129,17 @@ export class TimelineActivityWorkspaceEntity extends BaseWorkspaceEntity { inverseSideTarget: () => PersonWorkspaceEntity, inverseSideFieldKey: 'timelineActivities', onDelete: RelationOnDeleteAction.CASCADE, + isMorphRelation: true, + morphId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.targetMorphId, }) @WorkspaceIsNullable() - person: Relation | null; + targetPerson: Relation | null; - @WorkspaceJoinColumn('person') - personId: string | null; + @WorkspaceJoinColumn('targetPerson') + targetPersonId: string | null; @WorkspaceRelation({ - standardId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.company, + standardId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.targetCompany, type: RelationType.MANY_TO_ONE, label: msg`Company`, description: msg`Event company`, @@ -144,15 +147,17 @@ export class TimelineActivityWorkspaceEntity extends BaseWorkspaceEntity { inverseSideTarget: () => CompanyWorkspaceEntity, inverseSideFieldKey: 'timelineActivities', onDelete: RelationOnDeleteAction.CASCADE, + isMorphRelation: true, + morphId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.targetMorphId, }) @WorkspaceIsNullable() - company: Relation | null; + targetCompany: Relation | null; - @WorkspaceJoinColumn('company') - companyId: string | null; + @WorkspaceJoinColumn('targetCompany') + targetCompanyId: string | null; @WorkspaceRelation({ - standardId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.opportunity, + standardId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.targetOpportunity, type: RelationType.MANY_TO_ONE, label: msg`Opportunity`, description: msg`Event opportunity`, @@ -160,15 +165,17 @@ export class TimelineActivityWorkspaceEntity extends BaseWorkspaceEntity { inverseSideTarget: () => OpportunityWorkspaceEntity, inverseSideFieldKey: 'timelineActivities', onDelete: RelationOnDeleteAction.SET_NULL, + isMorphRelation: true, + morphId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.targetMorphId, }) @WorkspaceIsNullable() - opportunity: Relation | null; + targetOpportunity: Relation | null; - @WorkspaceJoinColumn('opportunity') - opportunityId: string | null; + @WorkspaceJoinColumn('targetOpportunity') + targetOpportunityId: string | null; @WorkspaceRelation({ - standardId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.note, + standardId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.targetNote, type: RelationType.MANY_TO_ONE, label: msg`Note`, description: msg`Event note`, @@ -176,15 +183,17 @@ export class TimelineActivityWorkspaceEntity extends BaseWorkspaceEntity { inverseSideTarget: () => NoteWorkspaceEntity, inverseSideFieldKey: 'timelineActivities', onDelete: RelationOnDeleteAction.SET_NULL, + isMorphRelation: true, + morphId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.targetMorphId, }) @WorkspaceIsNullable() - note: Relation | null; + targetNote: Relation | null; - @WorkspaceJoinColumn('note') - noteId: string | null; + @WorkspaceJoinColumn('targetNote') + targetNoteId: string | null; @WorkspaceRelation({ - standardId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.task, + standardId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.targetTask, type: RelationType.MANY_TO_ONE, label: msg`Task`, description: msg`Event task`, @@ -192,15 +201,17 @@ export class TimelineActivityWorkspaceEntity extends BaseWorkspaceEntity { inverseSideTarget: () => TaskWorkspaceEntity, inverseSideFieldKey: 'timelineActivities', onDelete: RelationOnDeleteAction.SET_NULL, + isMorphRelation: true, + morphId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.targetMorphId, }) @WorkspaceIsNullable() - task: Relation | null; + targetTask: Relation | null; - @WorkspaceJoinColumn('task') - taskId: string | null; + @WorkspaceJoinColumn('targetTask') + targetTaskId: string | null; @WorkspaceRelation({ - standardId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.workflow, + standardId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.targetWorkflow, type: RelationType.MANY_TO_ONE, label: msg`Workflow`, description: msg`Event workflow`, @@ -208,15 +219,17 @@ export class TimelineActivityWorkspaceEntity extends BaseWorkspaceEntity { inverseSideTarget: () => WorkflowWorkspaceEntity, inverseSideFieldKey: 'timelineActivities', onDelete: RelationOnDeleteAction.CASCADE, + isMorphRelation: true, + morphId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.targetMorphId, }) @WorkspaceIsNullable() - workflow: Relation | null; + targetWorkflow: Relation | null; - @WorkspaceJoinColumn('workflow') - workflowId: string | null; + @WorkspaceJoinColumn('targetWorkflow') + targetWorkflowId: string | null; @WorkspaceRelation({ - standardId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.workflowVersion, + standardId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.targetWorkflowVersion, type: RelationType.MANY_TO_ONE, label: msg`WorkflowVersion`, description: msg`Event workflow version`, @@ -224,15 +237,17 @@ export class TimelineActivityWorkspaceEntity extends BaseWorkspaceEntity { inverseSideTarget: () => WorkflowVersionWorkspaceEntity, inverseSideFieldKey: 'timelineActivities', onDelete: RelationOnDeleteAction.CASCADE, + isMorphRelation: true, + morphId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.targetMorphId, }) @WorkspaceIsNullable() - workflowVersion: Relation | null; + targetWorkflowVersion: Relation | null; - @WorkspaceJoinColumn('workflowVersion') - workflowVersionId: string | null; + @WorkspaceJoinColumn('targetWorkflowVersion') + targetWorkflowVersionId: string | null; @WorkspaceRelation({ - standardId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.workflowRun, + standardId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.targetWorkflowRun, type: RelationType.MANY_TO_ONE, label: msg`Workflow Run`, description: msg`Event workflow run`, @@ -240,15 +255,17 @@ export class TimelineActivityWorkspaceEntity extends BaseWorkspaceEntity { inverseSideTarget: () => WorkflowRunWorkspaceEntity, inverseSideFieldKey: 'timelineActivities', onDelete: RelationOnDeleteAction.CASCADE, + isMorphRelation: true, + morphId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.targetMorphId, }) @WorkspaceIsNullable() - workflowRun: Relation | null; + targetWorkflowRun: Relation | null; - @WorkspaceJoinColumn('workflowRun') - workflowRunId: string | null; + @WorkspaceJoinColumn('targetWorkflowRun') + targetWorkflowRunId: string | null; @WorkspaceRelation({ - standardId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.dashboard, + standardId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.targetDashboard, type: RelationType.MANY_TO_ONE, label: msg`Dashboard`, description: msg`Event dashboard`, @@ -256,17 +273,20 @@ export class TimelineActivityWorkspaceEntity extends BaseWorkspaceEntity { inverseSideTarget: () => DashboardWorkspaceEntity, inverseSideFieldKey: 'timelineActivities', onDelete: RelationOnDeleteAction.SET_NULL, + isMorphRelation: true, + morphId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.targetMorphId, }) @WorkspaceIsNullable() - dashboard: Relation | null; + targetDashboard: Relation | null; - @WorkspaceJoinColumn('dashboard') - dashboardId: string | null; + @WorkspaceJoinColumn('targetDashboard') + targetDashboardId: string | null; + // todo: remove this decorator and the custom field @WorkspaceDynamicRelation({ type: RelationType.MANY_TO_ONE, argsFactory: (oppositeObjectMetadata) => ({ - standardId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.custom, + standardId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.targetCustom, name: oppositeObjectMetadata.nameSingular, label: oppositeObjectMetadata.labelSingular, description: `Timeline Activity ${oppositeObjectMetadata.labelSingular}`, @@ -278,4 +298,20 @@ export class TimelineActivityWorkspaceEntity extends BaseWorkspaceEntity { onDelete: RelationOnDeleteAction.CASCADE, }) custom: Relation; + + @WorkspaceDynamicRelation({ + type: RelationType.MANY_TO_ONE, + argsFactory: (oppositeObjectMetadata) => ({ + standardId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.targetCustom, + name: oppositeObjectMetadata.nameSingular, + label: oppositeObjectMetadata.labelSingular, + description: `Timeline Activity ${oppositeObjectMetadata.labelSingular}`, + joinColumn: `target${capitalize(oppositeObjectMetadata.nameSingular)}Id`, + icon: 'IconTimeline', + }), + inverseSideTarget: () => CustomWorkspaceEntity, + inverseSideFieldKey: 'timelineActivities', + onDelete: RelationOnDeleteAction.CASCADE, + }) + targetCustom: Relation; } diff --git a/packages/twenty-server/src/modules/timeline/timeline-activity.module.ts b/packages/twenty-server/src/modules/timeline/timeline-activity.module.ts index 03c37b2cf3e..bc35c9d4339 100644 --- a/packages/twenty-server/src/modules/timeline/timeline-activity.module.ts +++ b/packages/twenty-server/src/modules/timeline/timeline-activity.module.ts @@ -1,5 +1,6 @@ import { Module } from '@nestjs/common'; +import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module'; import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module'; import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module'; @@ -12,6 +13,7 @@ import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-o TimelineActivityWorkspaceEntity, ]), TwentyORMModule, + FeatureFlagModule, WorkspaceManyOrAllFlatEntityMapsCacheModule, ], providers: [TimelineActivityService], diff --git a/packages/twenty-server/src/modules/timeline/utils/timeline-activity-related-morph-field-metadata-name-builder.util.ts b/packages/twenty-server/src/modules/timeline/utils/timeline-activity-related-morph-field-metadata-name-builder.util.ts new file mode 100644 index 00000000000..fc69d57492b --- /dev/null +++ b/packages/twenty-server/src/modules/timeline/utils/timeline-activity-related-morph-field-metadata-name-builder.util.ts @@ -0,0 +1,7 @@ +import { capitalize } from 'twenty-shared/utils'; + +export const buildTimelineActivityRelatedMorphFieldMetadataName = ( + name: string, +) => { + return `target${capitalize(name)}`; +}; diff --git a/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow-run.workspace-entity.ts b/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow-run.workspace-entity.ts index 2345a3388c0..cbe2e01afb8 100644 --- a/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow-run.workspace-entity.ts +++ b/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow-run.workspace-entity.ts @@ -284,6 +284,7 @@ export class WorkflowRunWorkspaceEntity extends BaseWorkspaceEntity { label: msg`Timeline Activities`, description: msg`Timeline activities linked to the run`, inverseSideTarget: () => TimelineActivityWorkspaceEntity, + inverseSideFieldKey: 'targetWorkflowRun', onDelete: RelationOnDeleteAction.CASCADE, }) @WorkspaceIsSystem() diff --git a/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow-version.workspace-entity.ts b/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow-version.workspace-entity.ts index e91c330cd30..fc7b33e3c66 100644 --- a/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow-version.workspace-entity.ts +++ b/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow-version.workspace-entity.ts @@ -198,6 +198,7 @@ export class WorkflowVersionWorkspaceEntity extends BaseWorkspaceEntity { label: msg`Timeline Activities`, description: msg`Timeline activities linked to the version`, inverseSideTarget: () => TimelineActivityWorkspaceEntity, + inverseSideFieldKey: 'targetWorkflowVersion', onDelete: RelationOnDeleteAction.CASCADE, }) @WorkspaceIsSystem() diff --git a/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow.workspace-entity.ts b/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow.workspace-entity.ts index d0f2590fcc8..b53df36e34e 100644 --- a/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow.workspace-entity.ts +++ b/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow.workspace-entity.ts @@ -9,9 +9,9 @@ import { import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; -import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/search-field-metadata/constants/search-vector-field.constants'; import { type FieldMetadataComplexOption } from 'src/engine/metadata-modules/field-metadata/dtos/options.input'; import { IndexType } from 'src/engine/metadata-modules/index-metadata/types/indexType.types'; +import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/search-field-metadata/constants/search-vector-field.constants'; import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator'; import { WorkspaceFieldIndex } from 'src/engine/twenty-orm/decorators/workspace-field-index.decorator'; @@ -193,6 +193,7 @@ export class WorkflowWorkspaceEntity extends BaseWorkspaceEntity { label: msg`Timeline Activities`, description: msg`Timeline activities linked to the workflow`, inverseSideTarget: () => TimelineActivityWorkspaceEntity, + inverseSideFieldKey: 'targetWorkflow', onDelete: RelationOnDeleteAction.CASCADE, }) @WorkspaceIsSystem() diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/timeline-activities.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/timeline-activities.integration-spec.ts index 69c7c8b0119..a6989921ce6 100644 --- a/packages/twenty-server/test/integration/graphql/suites/object-generated/timeline-activities.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/timeline-activities.integration-spec.ts @@ -21,16 +21,16 @@ describe('timelineActivitiesResolver (e2e)', () => { updatedAt deletedAt workspaceMemberId - personId - companyId - opportunityId - noteId - taskId - workflowId - workflowVersionId - workflowRunId - petId - surveyResultId + targetPersonId + targetCompanyId + targetOpportunityId + targetNoteId + targetTaskId + targetWorkflowId + targetWorkflowVersionId + targetWorkflowRunId + targetPetId + targetSurveyResultId } } } @@ -69,16 +69,16 @@ describe('timelineActivitiesResolver (e2e)', () => { expect(timelineActivities).toHaveProperty('updatedAt'); expect(timelineActivities).toHaveProperty('deletedAt'); expect(timelineActivities).toHaveProperty('workspaceMemberId'); - expect(timelineActivities).toHaveProperty('personId'); - expect(timelineActivities).toHaveProperty('companyId'); - expect(timelineActivities).toHaveProperty('opportunityId'); - expect(timelineActivities).toHaveProperty('noteId'); - expect(timelineActivities).toHaveProperty('taskId'); - expect(timelineActivities).toHaveProperty('workflowId'); - expect(timelineActivities).toHaveProperty('workflowVersionId'); - expect(timelineActivities).toHaveProperty('workflowRunId'); - expect(timelineActivities).toHaveProperty('petId'); - expect(timelineActivities).toHaveProperty('surveyResultId'); + expect(timelineActivities).toHaveProperty('targetPersonId'); + expect(timelineActivities).toHaveProperty('targetCompanyId'); + expect(timelineActivities).toHaveProperty('targetOpportunityId'); + expect(timelineActivities).toHaveProperty('targetNoteId'); + expect(timelineActivities).toHaveProperty('targetTaskId'); + expect(timelineActivities).toHaveProperty('targetWorkflowId'); + expect(timelineActivities).toHaveProperty('targetWorkflowVersionId'); + expect(timelineActivities).toHaveProperty('targetWorkflowRunId'); + expect(timelineActivities).toHaveProperty('targetPetId'); + expect(timelineActivities).toHaveProperty('targetSurveyResultId'); } }); }); diff --git a/packages/twenty-server/test/integration/metadata/suites/field-metadata/morph-relation/__snapshots__/successful-update-one-field-metadata-morph-relation-v2.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/field-metadata/morph-relation/__snapshots__/successful-update-one-field-metadata-morph-relation-v2.integration-spec.ts.snap index e1c7c806809..9d767bbf44b 100644 --- a/packages/twenty-server/test/integration/metadata/suites/field-metadata/morph-relation/__snapshots__/successful-update-one-field-metadata-morph-relation-v2.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/metadata/suites/field-metadata/morph-relation/__snapshots__/successful-update-one-field-metadata-morph-relation-v2.integration-spec.ts.snap @@ -21,7 +21,7 @@ exports[`updateOne FieldMetadataService morph relation fields v2 It should updat exports[`updateOne FieldMetadataService morph relation fields v2 It should update all morph related flat field metadata and their related field allowing its deletion 2`] = ` { "description": [ - null, + "Description for all", ], "isActive": [ true, @@ -58,7 +58,7 @@ exports[`updateOne FieldMetadataService morph relation fields v2 It should updat exports[`updateOne FieldMetadataService morph relation fields v2 It should update all morph related flat field metadata and their related field allowing its deletion 4`] = ` { "description": [ - null, + "Description for all", ], "isActive": [ false, diff --git a/packages/twenty-server/test/integration/metadata/suites/object-metadata/__snapshots__/failing-create-one-object-metadata-v2.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/object-metadata/__snapshots__/failing-create-one-object-metadata-v2.integration-spec.ts.snap index 5306078346c..657b329f324 100644 --- a/packages/twenty-server/test/integration/metadata/suites/object-metadata/__snapshots__/failing-create-one-object-metadata-v2.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/metadata/suites/object-metadata/__snapshots__/failing-create-one-object-metadata-v2.integration-spec.ts.snap @@ -227,7 +227,7 @@ exports[`Object metadata creation should fail v2 when labelPlural contains only ], "flatEntityMinimalInformation": { "id": Any, - "name": "listinga", + "name": "targetListinga", "objectMetadataId": Any, }, "status": "fail", @@ -853,7 +853,7 @@ exports[`Object metadata creation should fail v2 when labelPlural exceeds maximu ], "flatEntityMinimalInformation": { "id": Any, - "name": "listinga", + "name": "targetListinga", "objectMetadataId": Any, }, "status": "fail", @@ -1490,7 +1490,7 @@ exports[`Object metadata creation should fail v2 when labelSingular contains onl ], "flatEntityMinimalInformation": { "id": Any, - "name": "listinga", + "name": "targetListinga", "objectMetadataId": Any, }, "status": "fail", @@ -2116,7 +2116,7 @@ exports[`Object metadata creation should fail v2 when labelSingular exceeds maxi ], "flatEntityMinimalInformation": { "id": Any, - "name": "listinga", + "name": "targetListinga", "objectMetadataId": Any, }, "status": "fail", @@ -2753,7 +2753,7 @@ exports[`Object metadata creation should fail v2 when labels are identical 1`] = ], "flatEntityMinimalInformation": { "id": Any, - "name": "listinga", + "name": "targetListinga", "objectMetadataId": Any, }, "status": "fail", @@ -3379,7 +3379,7 @@ exports[`Object metadata creation should fail v2 when labels with whitespaces re ], "flatEntityMinimalInformation": { "id": Any, - "name": "listinga", + "name": "targetListinga", "objectMetadataId": Any, }, "status": "fail", @@ -4001,7 +4001,7 @@ exports[`Object metadata creation should fail v2 when name exceeds maximum lengt "code": "INVALID_FIELD_INPUT", "message": "Name is too long", "userFriendlyMessage": "Name is too long", - "value": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "value": "targetAaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", }, { "code": "OBJECT_METADATA_NOT_FOUND", @@ -4011,7 +4011,7 @@ exports[`Object metadata creation should fail v2 when name exceeds maximum lengt ], "flatEntityMinimalInformation": { "id": Any, - "name": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "name": "targetAaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "objectMetadataId": Any, }, "status": "fail", @@ -4661,7 +4661,7 @@ exports[`Object metadata creation should fail v2 when namePlural has invalid cha ], "flatEntityMinimalInformation": { "id": Any, - "name": "listinga", + "name": "targetListinga", "objectMetadataId": Any, }, "status": "fail", @@ -5287,7 +5287,7 @@ exports[`Object metadata creation should fail v2 when namePlural is a reserved k ], "flatEntityMinimalInformation": { "id": Any, - "name": "listinga", + "name": "targetListinga", "objectMetadataId": Any, }, "status": "fail", @@ -5924,7 +5924,7 @@ exports[`Object metadata creation should fail v2 when namePlural is not camelCas ], "flatEntityMinimalInformation": { "id": Any, - "name": "listinga", + "name": "targetListinga", "objectMetadataId": Any, }, "status": "fail", @@ -6552,13 +6552,13 @@ exports[`Object metadata creation should fail v2 when nameSingular contains only "code": "INVALID_FIELD_INPUT", "message": "Name should be in camelCase", "userFriendlyMessage": "Name should be in camelCase", - "value": "a a", + "value": "targetA a", }, { "code": "INVALID_FIELD_INPUT", "message": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", "userFriendlyMessage": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", - "value": "a a", + "value": "targetA a", }, { "code": "OBJECT_METADATA_NOT_FOUND", @@ -6568,7 +6568,7 @@ exports[`Object metadata creation should fail v2 when nameSingular contains only ], "flatEntityMinimalInformation": { "id": Any, - "name": "a a", + "name": "targetA a", "objectMetadataId": Any, }, "status": "fail", @@ -7240,18 +7240,6 @@ exports[`Object metadata creation should fail v2 when nameSingular contains only }, { "errors": [ - { - "code": "INVALID_FIELD_INPUT", - "message": "Name is too short", - "userFriendlyMessage": "Name is too short", - "value": "", - }, - { - "code": "INVALID_FIELD_INPUT", - "message": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", - "userFriendlyMessage": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", - "value": "", - }, { "code": "OBJECT_METADATA_NOT_FOUND", "message": "Relation target object metadata not found", @@ -7260,7 +7248,7 @@ exports[`Object metadata creation should fail v2 when nameSingular contains only ], "flatEntityMinimalInformation": { "id": Any, - "name": "", + "name": "target", "objectMetadataId": Any, }, "status": "fail", @@ -7932,11 +7920,17 @@ exports[`Object metadata creation should fail v2 when nameSingular has invalid c }, { "errors": [ + { + "code": "INVALID_FIELD_INPUT", + "message": "Name should be in camelCase", + "userFriendlyMessage": "Name should be in camelCase", + "value": "targetΜ", + }, { "code": "INVALID_FIELD_INPUT", "message": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", "userFriendlyMessage": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", - "value": "μ", + "value": "targetΜ", }, { "code": "OBJECT_METADATA_NOT_FOUND", @@ -7946,7 +7940,7 @@ exports[`Object metadata creation should fail v2 when nameSingular has invalid c ], "flatEntityMinimalInformation": { "id": Any, - "name": "μ", + "name": "targetΜ", "objectMetadataId": Any, }, "status": "fail", @@ -8588,12 +8582,6 @@ exports[`Object metadata creation should fail v2 when nameSingular is a reserved }, { "errors": [ - { - "code": "INVALID_FIELD_INPUT", - "message": "This name is reserved. Use a different name or the system will add "Custom" suffix.", - "userFriendlyMessage": "This name is reserved. Use a different name or the system will add "Custom" suffix.", - "value": "user", - }, { "code": "OBJECT_METADATA_NOT_FOUND", "message": "Relation target object metadata not found", @@ -8602,7 +8590,7 @@ exports[`Object metadata creation should fail v2 when nameSingular is a reserved ], "flatEntityMinimalInformation": { "id": Any, - "name": "user", + "name": "targetUser", "objectMetadataId": Any, }, "status": "fail", @@ -9259,13 +9247,13 @@ exports[`Object metadata creation should fail v2 when nameSingular is not camelC "code": "INVALID_FIELD_INPUT", "message": "Name should be in camelCase", "userFriendlyMessage": "Name should be in camelCase", - "value": "Not_Camel_Case", + "value": "targetNot_Camel_Case", }, { "code": "INVALID_FIELD_INPUT", "message": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", "userFriendlyMessage": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", - "value": "Not_Camel_Case", + "value": "targetNot_Camel_Case", }, { "code": "OBJECT_METADATA_NOT_FOUND", @@ -9275,7 +9263,7 @@ exports[`Object metadata creation should fail v2 when nameSingular is not camelC ], "flatEntityMinimalInformation": { "id": Any, - "name": "Not_Camel_Case", + "name": "targetNot_Camel_Case", "objectMetadataId": Any, }, "status": "fail", @@ -9955,7 +9943,7 @@ exports[`Object metadata creation should fail v2 when names are identical 1`] = ], "flatEntityMinimalInformation": { "id": Any, - "name": "fooBar", + "name": "targetFooBar", "objectMetadataId": Any, }, "status": "fail", @@ -10581,7 +10569,7 @@ exports[`Object metadata creation should fail v2 when names with whitespaces res ], "flatEntityMinimalInformation": { "id": Any, - "name": "fooBar", + "name": "targetFooBar", "objectMetadataId": Any, }, "status": "fail", diff --git a/packages/twenty-server/test/integration/metadata/suites/object-metadata/rename-custom-object.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/object-metadata/rename-custom-object.integration-spec.ts index 88580853a69..09cc2063aa8 100644 --- a/packages/twenty-server/test/integration/metadata/suites/object-metadata/rename-custom-object.integration-spec.ts +++ b/packages/twenty-server/test/integration/metadata/suites/object-metadata/rename-custom-object.integration-spec.ts @@ -5,6 +5,7 @@ import { findManyObjectMetadataQueryFactory } from 'test/integration/metadata/su import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util'; import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util'; import { FieldMetadataType } from 'twenty-shared/types'; +import { capitalize } from 'twenty-shared/utils'; describe('Custom object renaming', () => { let listingObjectId = ''; @@ -66,7 +67,9 @@ describe('Custom object renaming', () => { standardObjectRelationsMap[relation].objectMetadataId = standardObjects.body.data.objects.edges.find( // @ts-expect-error legacy noImplicitAny - (object) => object.node.nameSingular === relation, + (object) => + object.node.nameSingular === relation || + object.node.nameSingular === `target${relation}`, ).node.id; }); }; @@ -110,21 +113,26 @@ describe('Custom object renaming', () => { .filter( // @ts-expect-error legacy noImplicitAny (field) => - field.node.name === `${CUSTOM_OBJECT.nameSingular}` && - field.node.type === FieldMetadataType.RELATION, + (field.node.name === `${CUSTOM_OBJECT.nameSingular}` && + FieldMetadataType.RELATION) || + (field.node.name === + `target${capitalize(CUSTOM_OBJECT.nameSingular)}` && + FieldMetadataType.MORPH_RELATION), ) // @ts-expect-error legacy noImplicitAny .map((field) => field.node); STANDARD_OBJECT_RELATIONS.forEach((relation) => { // relation field - const relationFieldMetadataId = relationFieldsMetadataForListing.find( + const relationFieldMetadata = relationFieldsMetadataForListing.find( // @ts-expect-error legacy noImplicitAny (field) => field.object.id === // @ts-expect-error legacy noImplicitAny standardObjectRelationsMap[relation].objectMetadataId, - ).id; + ); + + const relationFieldMetadataId = relationFieldMetadata?.id; expect(relationFieldMetadataId).not.toBeUndefined();