From 8455ecc3e85ae5960526400bd6aad733c1bc646e Mon Sep 17 00:00:00 2001 From: Charles Bochet Date: Tue, 25 Nov 2025 15:27:58 +0100 Subject: [PATCH] Add import scheduled status to messaging sync (#16058) We have introduced to new syncStage statuses: `messageChannel.MESSAGES_IMPORT_SCHEDULED` and `calendarChannel.CALENDAR_EVENTS_IMPORT_SCHEDULED` We need to make sure all existing workspaces have it --- ...nts-import-scheduled-sync-stage.command.ts | 165 ++++++++++++++++++ ...ges-import-scheduled-sync-stage.command.ts | 164 +++++++++++++++++ .../1-12-upgrade-version-command.module.ts | 16 +- .../upgrade.command.ts | 6 + .../services/field-metadata.service-v2.ts | 4 +- .../message-channel.workspace-entity.ts | 2 +- 6 files changed, 354 insertions(+), 3 deletions(-) create mode 100644 packages/twenty-server/src/database/commands/upgrade-version-command/1-12/1-12-add-calendar-events-import-scheduled-sync-stage.command.ts create mode 100644 packages/twenty-server/src/database/commands/upgrade-version-command/1-12/1-12-add-messages-import-scheduled-sync-stage.command.ts diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-12/1-12-add-calendar-events-import-scheduled-sync-stage.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-12/1-12-add-calendar-events-import-scheduled-sync-stage.command.ts new file mode 100644 index 0000000000..9f37eda232 --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-12/1-12-add-calendar-events-import-scheduled-sync-stage.command.ts @@ -0,0 +1,165 @@ +import { InjectRepository } from '@nestjs/typeorm'; + +import { Command } from 'nest-commander'; +import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata'; +import { Repository } from 'typeorm'; + +import { + ActiveOrSuspendedWorkspacesMigrationCommandRunner, + type RunOnWorkspaceArgs, +} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner'; +import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; +import { type FieldMetadataComplexOption } from 'src/engine/metadata-modules/field-metadata/dtos/options.input'; +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { FieldMetadataServiceV2 } from 'src/engine/metadata-modules/field-metadata/services/field-metadata.service-v2'; +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { CALENDAR_CHANNEL_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids'; +import { CalendarChannelSyncStage } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity'; + +@Command({ + name: 'upgrade:1-12:add-calendar-events-import-scheduled-sync-stage', + description: + 'Add CALENDAR_EVENTS_IMPORT_SCHEDULED sync stage to calendar channel syncStage field', +}) +export class AddCalendarEventsImportScheduledSyncStageCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner { + constructor( + @InjectRepository(WorkspaceEntity) + protected readonly workspaceRepository: Repository, + protected readonly twentyORMGlobalManager: TwentyORMGlobalManager, + @InjectRepository(ObjectMetadataEntity) + private readonly objectMetadataRepository: Repository, + @InjectRepository(FieldMetadataEntity) + private readonly fieldMetadataRepository: Repository, + private readonly fieldMetadataService: FieldMetadataServiceV2, + ) { + super(workspaceRepository, twentyORMGlobalManager); + } + + override async runOnWorkspace({ + workspaceId, + options, + }: RunOnWorkspaceArgs): Promise { + this.logger.log( + `Adding CALENDAR_EVENTS_IMPORT_SCHEDULED sync stage for workspace ${workspaceId}`, + ); + + const calendarChannelObject = await this.objectMetadataRepository.findOne({ + where: { + standardId: STANDARD_OBJECT_IDS.calendarChannel, + workspaceId, + }, + }); + + if (!calendarChannelObject) { + this.logger.log( + `CalendarChannel object not found for workspace ${workspaceId}, skipping enum migration`, + ); + + return; + } + + const syncStageField = await this.fieldMetadataRepository.findOne({ + where: { + standardId: CALENDAR_CHANNEL_STANDARD_FIELD_IDS.syncStage, + objectMetadataId: calendarChannelObject.id, + workspaceId, + }, + }); + + if (!syncStageField) { + this.logger.log( + `CalendarChannel syncStage field not found for workspace ${workspaceId}, skipping field metadata update`, + ); + + return; + } + + const fieldOptions = (syncStageField.options || + []) as FieldMetadataComplexOption[]; + + const hasCalendarEventsImportScheduled = fieldOptions.some( + (option) => + option.value === + CalendarChannelSyncStage.CALENDAR_EVENTS_IMPORT_SCHEDULED, + ); + + if (hasCalendarEventsImportScheduled) { + this.logger.log( + `CalendarChannel syncStage field metadata already has CALENDAR_EVENTS_IMPORT_SCHEDULED for workspace ${workspaceId}`, + ); + + return; + } + + if (options.dryRun) { + this.logger.log( + `Would add CALENDAR_EVENTS_IMPORT_SCHEDULED sync stage for workspace ${workspaceId}`, + ); + + return; + } + + await this.fieldMetadataService.updateOneField({ + updateFieldInput: { + id: syncStageField.id, + options: [ + { + value: CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_PENDING, + label: 'Calendar event list fetch pending', + position: 0, + color: 'blue', + }, + { + value: CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_SCHEDULED, + label: 'Calendar event list fetch scheduled', + position: 1, + color: 'green', + }, + { + value: CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_ONGOING, + label: 'Calendar event list fetch ongoing', + position: 2, + color: 'orange', + }, + { + value: CalendarChannelSyncStage.CALENDAR_EVENTS_IMPORT_PENDING, + label: 'Calendar events import pending', + position: 3, + color: 'blue', + }, + { + value: CalendarChannelSyncStage.CALENDAR_EVENTS_IMPORT_SCHEDULED, + label: 'Calendar events import scheduled', + position: 4, + color: 'green', + }, + { + value: CalendarChannelSyncStage.CALENDAR_EVENTS_IMPORT_ONGOING, + label: 'Calendar events import ongoing', + position: 5, + color: 'orange', + }, + { + value: CalendarChannelSyncStage.FAILED, + label: 'Failed', + position: 6, + color: 'red', + }, + { + value: CalendarChannelSyncStage.PENDING_CONFIGURATION, + label: 'Pending configuration', + position: 9, + color: 'gray', + }, + ], + }, + workspaceId, + isSystemBuild: true, + }); + + this.logger.log( + `Successfully added CALENDAR_EVENTS_IMPORT_SCHEDULED sync stage for workspace ${workspaceId}`, + ); + } +} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-12/1-12-add-messages-import-scheduled-sync-stage.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-12/1-12-add-messages-import-scheduled-sync-stage.command.ts new file mode 100644 index 0000000000..5fa4fd5533 --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-12/1-12-add-messages-import-scheduled-sync-stage.command.ts @@ -0,0 +1,164 @@ +import { InjectRepository } from '@nestjs/typeorm'; + +import { Command } from 'nest-commander'; +import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata'; +import { Repository } from 'typeorm'; + +import { + ActiveOrSuspendedWorkspacesMigrationCommandRunner, + type RunOnWorkspaceArgs, +} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner'; +import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; +import { type FieldMetadataComplexOption } from 'src/engine/metadata-modules/field-metadata/dtos/options.input'; +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { FieldMetadataServiceV2 } from 'src/engine/metadata-modules/field-metadata/services/field-metadata.service-v2'; +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { MESSAGE_CHANNEL_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids'; +import { MessageChannelSyncStage } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; + +@Command({ + name: 'upgrade:1-12:add-messages-import-scheduled-sync-stage', + description: + 'Add MESSAGES_IMPORT_SCHEDULED sync stage to message channel syncStage field', +}) +export class AddMessagesImportScheduledSyncStageCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner { + constructor( + @InjectRepository(WorkspaceEntity) + protected readonly workspaceRepository: Repository, + protected readonly twentyORMGlobalManager: TwentyORMGlobalManager, + @InjectRepository(ObjectMetadataEntity) + private readonly objectMetadataRepository: Repository, + @InjectRepository(FieldMetadataEntity) + private readonly fieldMetadataRepository: Repository, + private readonly fieldMetadataService: FieldMetadataServiceV2, + ) { + super(workspaceRepository, twentyORMGlobalManager); + } + + override async runOnWorkspace({ + workspaceId, + options, + }: RunOnWorkspaceArgs): Promise { + this.logger.log( + `Adding MESSAGES_IMPORT_SCHEDULED sync stage for workspace ${workspaceId}`, + ); + + const messageChannelObject = await this.objectMetadataRepository.findOne({ + where: { + standardId: STANDARD_OBJECT_IDS.messageChannel, + workspaceId, + }, + }); + + if (!messageChannelObject) { + this.logger.log( + `MessageChannel object not found for workspace ${workspaceId}, skipping enum migration`, + ); + + return; + } + + const syncStageField = await this.fieldMetadataRepository.findOne({ + where: { + standardId: MESSAGE_CHANNEL_STANDARD_FIELD_IDS.syncStage, + objectMetadataId: messageChannelObject.id, + workspaceId, + }, + }); + + if (!syncStageField) { + this.logger.log( + `MessageChannel syncStage field not found for workspace ${workspaceId}, skipping field metadata update`, + ); + + return; + } + + const fieldOptions = (syncStageField.options || + []) as FieldMetadataComplexOption[]; + + const hasMessagesImportScheduled = fieldOptions.some( + (option) => + option.value === MessageChannelSyncStage.MESSAGES_IMPORT_SCHEDULED, + ); + + if (hasMessagesImportScheduled) { + this.logger.log( + `MessageChannel syncStage field metadata already has MESSAGES_IMPORT_SCHEDULED for workspace ${workspaceId}`, + ); + + return; + } + + if (options.dryRun) { + this.logger.log( + `Would add MESSAGES_IMPORT_SCHEDULED sync stage for workspace ${workspaceId}`, + ); + + return; + } + + await this.fieldMetadataService.updateOneField({ + updateFieldInput: { + id: syncStageField.id, + options: [ + { + value: MessageChannelSyncStage.MESSAGE_LIST_FETCH_PENDING, + label: 'Messages list fetch pending', + position: 0, + color: 'blue', + }, + { + value: MessageChannelSyncStage.MESSAGE_LIST_FETCH_SCHEDULED, + label: 'Messages list fetch scheduled', + position: 1, + color: 'green', + }, + { + value: MessageChannelSyncStage.MESSAGE_LIST_FETCH_ONGOING, + label: 'Messages list fetch ongoing', + position: 2, + color: 'orange', + }, + { + value: MessageChannelSyncStage.MESSAGES_IMPORT_PENDING, + label: 'Messages import pending', + position: 3, + color: 'blue', + }, + { + value: MessageChannelSyncStage.MESSAGES_IMPORT_SCHEDULED, + label: 'Messages import scheduled', + position: 4, + color: 'green', + }, + { + value: MessageChannelSyncStage.MESSAGES_IMPORT_ONGOING, + label: 'Messages import ongoing', + position: 5, + color: 'orange', + }, + { + value: MessageChannelSyncStage.FAILED, + label: 'Failed', + position: 6, + color: 'red', + }, + { + value: MessageChannelSyncStage.PENDING_CONFIGURATION, + label: 'Pending configuration', + position: 7, + color: 'gray', + }, + ], + }, + workspaceId, + isSystemBuild: true, + }); + + this.logger.log( + `Successfully added MESSAGES_IMPORT_SCHEDULED sync stage for workspace ${workspaceId}`, + ); + } +} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-12/1-12-upgrade-version-command.module.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-12/1-12-upgrade-version-command.module.ts index 03ec7b862c..d1bdb3394f 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-12/1-12-upgrade-version-command.module.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-12/1-12-upgrade-version-command.module.ts @@ -1,25 +1,39 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; +import { AddCalendarEventsImportScheduledSyncStageCommand } from 'src/database/commands/upgrade-version-command/1-12/1-12-add-calendar-events-import-scheduled-sync-stage.command'; +import { AddMessagesImportScheduledSyncStageCommand } from 'src/database/commands/upgrade-version-command/1-12/1-12-add-messages-import-scheduled-sync-stage.command'; import { CreateWorkspaceCustomApplicationCommand } from 'src/database/commands/upgrade-version-command/1-12/1-12-create-workspace-custom-application.command'; import { SetStandardApplicationNotUninstallableCommand } from 'src/database/commands/upgrade-version-command/1-12/1-12-set-standard-application-not-uninstallable.command'; import { WorkspaceCustomApplicationIdNonNullableCommand } from 'src/database/commands/upgrade-version-command/1-12/1-12-workspace-custom-application-id-non-nullable-migration.command'; import { ApplicationModule } from 'src/engine/core-modules/application/application.module'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { FieldMetadataModule } from 'src/engine/metadata-modules/field-metadata/field-metadata.module'; +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { WorkspaceSchemaManagerModule } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.module'; @Module({ imports: [ - TypeOrmModule.forFeature([WorkspaceEntity]), + TypeOrmModule.forFeature([ + WorkspaceEntity, + ObjectMetadataEntity, + FieldMetadataEntity, + ]), WorkspaceSchemaManagerModule, ApplicationModule, + FieldMetadataModule, ], providers: [ + AddCalendarEventsImportScheduledSyncStageCommand, + AddMessagesImportScheduledSyncStageCommand, CreateWorkspaceCustomApplicationCommand, SetStandardApplicationNotUninstallableCommand, WorkspaceCustomApplicationIdNonNullableCommand, ], exports: [ + AddCalendarEventsImportScheduledSyncStageCommand, + AddMessagesImportScheduledSyncStageCommand, CreateWorkspaceCustomApplicationCommand, SetStandardApplicationNotUninstallableCommand, WorkspaceCustomApplicationIdNonNullableCommand, 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 08d5aedac2..e72e9d2001 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 @@ -21,6 +21,8 @@ import { RegenerateSearchVectorsCommand } from 'src/database/commands/upgrade-ve import { CleanOrphanedRoleTargetsCommand } from 'src/database/commands/upgrade-version-command/1-11/1-11-clean-orphaned-role-targets.command'; import { CleanOrphanedUserWorkspacesCommand } from 'src/database/commands/upgrade-version-command/1-11/1-11-clean-orphaned-user-workspaces.command'; import { CreateTwentyStandardApplicationCommand } from 'src/database/commands/upgrade-version-command/1-11/1-11-create-twenty-standard-application.command'; +import { AddCalendarEventsImportScheduledSyncStageCommand } from 'src/database/commands/upgrade-version-command/1-12/1-12-add-calendar-events-import-scheduled-sync-stage.command'; +import { AddMessagesImportScheduledSyncStageCommand } from 'src/database/commands/upgrade-version-command/1-12/1-12-add-messages-import-scheduled-sync-stage.command'; import { CreateWorkspaceCustomApplicationCommand } from 'src/database/commands/upgrade-version-command/1-12/1-12-create-workspace-custom-application.command'; import { SetStandardApplicationNotUninstallableCommand } from 'src/database/commands/upgrade-version-command/1-12/1-12-set-standard-application-not-uninstallable.command'; import { WorkspaceCustomApplicationIdNonNullableCommand } from 'src/database/commands/upgrade-version-command/1-12/1-12-workspace-custom-application-id-non-nullable-migration.command'; @@ -84,6 +86,8 @@ export class UpgradeCommand extends UpgradeCommandRunner { protected readonly createTwentyStandardApplicationCommand: CreateTwentyStandardApplicationCommand, protected readonly createWorkspaceCustomApplicationCommand: CreateWorkspaceCustomApplicationCommand, protected readonly workspaceCustomApplicationIdNonNullableCommand: WorkspaceCustomApplicationIdNonNullableCommand, + protected readonly addMessagesImportScheduledSyncStageCommand: AddMessagesImportScheduledSyncStageCommand, + protected readonly addCalendarEventsImportScheduledSyncStageCommand: AddCalendarEventsImportScheduledSyncStageCommand, ) { super( workspaceRepository, @@ -143,6 +147,8 @@ export class UpgradeCommand extends UpgradeCommandRunner { beforeSyncMetadata: [ this.createWorkspaceCustomApplicationCommand, this.workspaceCustomApplicationIdNonNullableCommand, + this.addMessagesImportScheduledSyncStageCommand, + this.addCalendarEventsImportScheduledSyncStageCommand, ], afterSyncMetadata: [this.setStandardApplicationNotUninstallableCommand], }; diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata.service-v2.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata.service-v2.ts index 6970f556d8..91f1b702b8 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata.service-v2.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata.service-v2.ts @@ -146,9 +146,11 @@ export class FieldMetadataServiceV2 extends TypeOrmQueryService; workspaceId: string; + isSystemBuild?: boolean; }): Promise { const { flatObjectMetadataMaps: existingFlatObjectMetadataMaps, @@ -256,7 +258,7 @@ export class FieldMetadataServiceV2 extends TypeOrmQueryService