From b83dc3aaff8d5c893e06534f0ac98aeb9bb09856 Mon Sep 17 00:00:00 2001 From: Guillim Date: Thu, 11 Dec 2025 14:28:26 +0100 Subject: [PATCH] TimelineActivity migration to morph (#15652) # TimelineActivity migration to morph - Creates `timelineActivities2` relations on Company, Dashboard, Note, Opportunity, Person, Task, Workflow, WorkflowRun, and WorkflowVersion entities with proper metadata and cascade delete behavior. It was required to create standard fields as well since the mapObjectMetadataByUniqueIdentifier needs it. otherwise the fields won't be considered - Feature Flag `IS_TIMELINE_ACTIVITY_MIGRATED` necessary to have the two states in parallel. It is used as a stamp once the migration has been run - Migration is done using the coreDataSource. Why ? even though is unsafe to use, the first implementation of the migration took forever on each workspace. See [this commit](https://github.com/twentyhq/twenty/pull/15652/commits/477011e8d7d4c580f79ba7ec4a8fb002a3ec86b2) The plan for this complex migration is as follows : ![plan](https://github.com/user-attachments/assets/51c63ea6-fb0d-40b4-b99d-3e0b35a204e6) Note: we will need to rename fields in the release 1.12 (there is no easy way to do all this in one release) --- .../src/generated-metadata/graphql.ts | 1 + .../twenty-front/src/generated/graphql.ts | 1 + .../hooks/useTimelineActivities.ts | 16 +- ...ine-activity-to-morph-relations.command.ts | 267 ++++++++++++++ .../1-13-upgrade-version-command.module.ts | 17 +- .../upgrade.command.ts | 3 + .../enums/feature-flag-key.enum.ts | 1 + ...lat-field-metadatas-to-create.spec.ts.snap | 12 +- ...-relation-flat-field-metadata-pair.spec.ts | 330 ++++++++++++++++++ ...from-morph-relation-update-payload.util.ts | 1 + ...ield-input-to-flat-field-metadatas.util.ts | 3 +- ...ield-input-to-flat-field-metadatas.util.ts | 3 +- ...-relation-flat-field-metadata-pair.util.ts | 64 +++- ...d-metadata-from-create-field-input.util.ts | 2 +- ...morph-relation-flat-field-metadata.util.ts | 8 + ...and-flat-field-metadatas-to-create.util.ts | 6 + .../object-metadata.service.ts | 15 +- ...-field-metadatas-for-custom-object.util.ts | 321 +++++++---------- .../remote-table-relations.service.ts | 6 +- .../twenty-orm/custom.workspace-entity.ts | 1 + .../workspace-entity-manager.spec.ts | 1 + .../core/utils/seed-feature-flags.util.ts | 5 + .../timeline-activity-seeder.service.ts | 72 ++-- .../constants/standard-object.constant.ts | 48 +-- ...mpany-standard-flat-field-metadata.util.ts | 2 +- ...board-standard-flat-field-metadata.util.ts | 2 +- ...-note-standard-flat-field-metadata.util.ts | 2 +- ...unity-standard-flat-field-metadata.util.ts | 2 +- ...erson-standard-flat-field-metadata.util.ts | 2 +- ...-task-standard-flat-field-metadata.util.ts | 2 +- ...ivity-standard-flat-field-metadata.util.ts | 63 ++-- ...w-run-standard-flat-field-metadata.util.ts | 2 +- ...kflow-standard-flat-field-metadata.util.ts | 2 +- ...rsion-standard-flat-field-metadata.util.ts | 2 +- ...morph-relation-field-flat-metadata.util.ts | 41 +++ ...ndard-relation-field-flat-metadata.util.ts | 8 +- ...ivity-standard-flat-index-metadata.util.ts | 18 +- .../constants/default-feature-flags.ts | 6 +- .../constants/standard-field-ids.ts | 21 +- .../attachment.workspace-entity.ts | 1 + ...lendar-event-participant-manager.module.ts | 17 +- .../calendar-event-participant.listener.ts | 12 +- .../company.workspace-entity.ts | 1 + .../dashboard.workspace-entity.ts | 1 + .../favorite.workspace-entity.ts | 3 +- .../listeners/message-participant.listener.ts | 12 +- .../message-participant-manager.module.ts | 14 + .../note-target.workspace-entity.ts | 3 +- .../standard-objects/note.workspace-entity.ts | 5 +- .../opportunity.workspace-entity.ts | 5 +- .../person.workspace-entity.ts | 1 + .../task-target.workspace-entity.ts | 3 +- .../standard-objects/task.workspace-entity.ts | 1 + .../timeline-activity.repository.ts | 40 ++- .../services/timeline-activity.service.ts | 12 +- .../timeline-activity.workspace-entity.ts | 110 ++++-- .../timeline/timeline-activity.module.ts | 2 + ...-morph-field-metadata-name-builder.util.ts | 7 + .../workflow-run.workspace-entity.ts | 1 + .../workflow-version.workspace-entity.ts | 1 + .../workflow.workspace-entity.ts | 3 +- .../timeline-activities.integration-spec.ts | 40 +-- ...morph-relation-v2.integration-spec.ts.snap | 4 +- ...bject-metadata-v2.integration-spec.ts.snap | 70 ++-- .../rename-custom-object.integration-spec.ts | 18 +- 65 files changed, 1299 insertions(+), 467 deletions(-) create mode 100644 packages/twenty-server/src/database/commands/upgrade-version-command/1-13/1-13-migrate-timeline-activity-to-morph-relations.command.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/__tests__/generate-morph-or-relation-flat-field-metadata-pair.spec.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/create-standard-morph-relation-field-flat-metadata.util.ts create mode 100644 packages/twenty-server/src/modules/timeline/utils/timeline-activity-related-morph-field-metadata-name-builder.util.ts 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();