Fix upgrade command messaging (#16067)

This commit is contained in:
Charles Bochet
2025-11-25 18:14:51 +01:00
committed by GitHub
parent f3416d435c
commit b1c03b533f
2 changed files with 349 additions and 163 deletions
@@ -1,26 +1,25 @@
import { InjectRepository } from '@nestjs/typeorm';
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata';
import { Repository } from 'typeorm';
import { DataSource, 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 { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util';
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',
'Replace calendar channel syncStage enum with complete CalendarChannelSyncStage values and update default to PENDING_CONFIGURATION',
})
export class AddCalendarEventsImportScheduledSyncStageCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
constructor(
@@ -31,7 +30,8 @@ export class AddCalendarEventsImportScheduledSyncStageCommand extends ActiveOrSu
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
@InjectRepository(FieldMetadataEntity)
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
private readonly fieldMetadataService: FieldMetadataServiceV2,
@InjectDataSource()
private readonly coreDataSource: DataSource,
) {
super(workspaceRepository, twentyORMGlobalManager);
}
@@ -41,9 +41,25 @@ export class AddCalendarEventsImportScheduledSyncStageCommand extends ActiveOrSu
options,
}: RunOnWorkspaceArgs): Promise<void> {
this.logger.log(
`Adding CALENDAR_EVENTS_IMPORT_SCHEDULED sync stage for workspace ${workspaceId}`,
`Updating calendar channel syncStage enum and default value for workspace ${workspaceId}`,
);
await this.updateCalendarChannelSyncStageFieldMetadata(
workspaceId,
options,
);
await this.addCalendarEventsImportScheduledEnumValue(workspaceId, options);
this.logger.log(
`Successfully updated calendar channel syncStage enum and default value for workspace ${workspaceId}`,
);
}
private async addCalendarEventsImportScheduledEnumValue(
workspaceId: string,
options: RunOnWorkspaceArgs['options'],
): Promise<void> {
const calendarChannelObject = await this.objectMetadataRepository.findOne({
where: {
standardId: STANDARD_OBJECT_IDS.calendarChannel,
@@ -59,6 +75,103 @@ export class AddCalendarEventsImportScheduledSyncStageCommand extends ActiveOrSu
return;
}
const schemaName = getWorkspaceSchemaName(workspaceId);
const tableName = 'calendarChannel';
const columnName = 'syncStage';
const enumName = 'calendarChannel_syncStage_enum';
if (options.dryRun) {
this.logger.log(
`Would replace ${enumName} with complete CalendarChannelSyncStage enum values for workspace ${workspaceId}`,
);
return;
}
const queryRunner = this.coreDataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
// Remove default value first
await queryRunner.query(
`ALTER TABLE "${schemaName}"."${tableName}" ALTER COLUMN "${columnName}" DROP DEFAULT`,
);
// Convert column to text to remove enum dependency
await queryRunner.query(
`ALTER TABLE "${schemaName}"."${tableName}" ALTER COLUMN "${columnName}" TYPE text USING "${columnName}"::text`,
);
// Drop the old enum (now safe since no column uses it)
await queryRunner.query(
`DROP TYPE IF EXISTS ${schemaName}."${enumName}" CASCADE`,
);
// Create new enum with all CalendarChannelSyncStage values
await queryRunner.query(
`CREATE TYPE ${schemaName}."${enumName}" AS ENUM (
'${CalendarChannelSyncStage.PENDING_CONFIGURATION}',
'${CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_PENDING}',
'${CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_SCHEDULED}',
'${CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_ONGOING}',
'${CalendarChannelSyncStage.CALENDAR_EVENTS_IMPORT_PENDING}',
'${CalendarChannelSyncStage.CALENDAR_EVENTS_IMPORT_SCHEDULED}',
'${CalendarChannelSyncStage.CALENDAR_EVENTS_IMPORT_ONGOING}',
'${CalendarChannelSyncStage.FAILED}'
)`,
);
// Convert column back to enum type
await queryRunner.query(
`ALTER TABLE "${schemaName}"."${tableName}"
ALTER COLUMN "${columnName}" TYPE ${schemaName}."${enumName}"
USING "${columnName}"::${schemaName}."${enumName}"`,
);
// Update default value to PENDING_CONFIGURATION
await queryRunner.query(
`ALTER TABLE "${schemaName}"."${tableName}"
ALTER COLUMN "${columnName}" SET DEFAULT '${CalendarChannelSyncStage.PENDING_CONFIGURATION}'::${schemaName}."${enumName}"`,
);
await queryRunner.commitTransaction();
this.logger.log(
`Successfully replaced ${enumName} with complete CalendarChannelSyncStage enum values and updated default to PENDING_CONFIGURATION for workspace ${workspaceId}`,
);
} catch (error) {
await queryRunner.rollbackTransaction();
this.logger.error(
`Error replacing ${enumName} for workspace ${workspaceId}: ${error}`,
);
throw error;
} finally {
await queryRunner.release();
}
}
private async updateCalendarChannelSyncStageFieldMetadata(
workspaceId: string,
options: RunOnWorkspaceArgs['options'],
): Promise<void> {
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 field metadata update`,
);
return;
}
const syncStageField = await this.fieldMetadataRepository.findOne({
where: {
standardId: CALENDAR_CHANNEL_STANDARD_FIELD_IDS.syncStage,
@@ -75,91 +188,72 @@ export class AddCalendarEventsImportScheduledSyncStageCommand extends ActiveOrSu
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}`,
`Would add CALENDAR_EVENTS_IMPORT_SCHEDULED to CalendarChannel syncStage field metadata 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',
},
],
const syncStageFieldOptions = [
{
value: CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_PENDING,
label: 'Calendar event list fetch pending',
position: 0,
color: 'blue',
},
workspaceId,
isSystemBuild: true,
});
{
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: 7,
color: 'gray',
},
];
syncStageField.options = syncStageFieldOptions;
syncStageField.defaultValue = `'${CalendarChannelSyncStage.PENDING_CONFIGURATION}'`;
await this.fieldMetadataRepository.save(syncStageField);
this.logger.log(
`Successfully added CALENDAR_EVENTS_IMPORT_SCHEDULED sync stage for workspace ${workspaceId}`,
`Added CALENDAR_EVENTS_IMPORT_SCHEDULED to CalendarChannel syncStage field metadata for workspace ${workspaceId}`,
);
}
}
@@ -1,26 +1,25 @@
import { InjectRepository } from '@nestjs/typeorm';
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata';
import { Repository } from 'typeorm';
import { DataSource, 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 { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util';
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',
'Replace message channel syncStage enum with complete MessageChannelSyncStage values and update default to PENDING_CONFIGURATION',
})
export class AddMessagesImportScheduledSyncStageCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
constructor(
@@ -31,7 +30,8 @@ export class AddMessagesImportScheduledSyncStageCommand extends ActiveOrSuspende
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
@InjectRepository(FieldMetadataEntity)
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
private readonly fieldMetadataService: FieldMetadataServiceV2,
@InjectDataSource()
private readonly coreDataSource: DataSource,
) {
super(workspaceRepository, twentyORMGlobalManager);
}
@@ -41,9 +41,22 @@ export class AddMessagesImportScheduledSyncStageCommand extends ActiveOrSuspende
options,
}: RunOnWorkspaceArgs): Promise<void> {
this.logger.log(
`Adding MESSAGES_IMPORT_SCHEDULED sync stage for workspace ${workspaceId}`,
`Updating message channel syncStage enum and default value for workspace ${workspaceId}`,
);
await this.updateMessageChannelSyncStageFieldMetadata(workspaceId, options);
await this.addMessagesImportScheduledEnumValue(workspaceId, options);
this.logger.log(
`Successfully updated message channel syncStage enum and default value for workspace ${workspaceId}`,
);
}
private async addMessagesImportScheduledEnumValue(
workspaceId: string,
options: RunOnWorkspaceArgs['options'],
): Promise<void> {
const messageChannelObject = await this.objectMetadataRepository.findOne({
where: {
standardId: STANDARD_OBJECT_IDS.messageChannel,
@@ -59,6 +72,103 @@ export class AddMessagesImportScheduledSyncStageCommand extends ActiveOrSuspende
return;
}
const schemaName = getWorkspaceSchemaName(workspaceId);
const tableName = 'messageChannel';
const columnName = 'syncStage';
const enumName = 'messageChannel_syncStage_enum';
if (options.dryRun) {
this.logger.log(
`Would replace ${enumName} with complete MessageChannelSyncStage enum values for workspace ${workspaceId}`,
);
return;
}
const queryRunner = this.coreDataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
// Remove default value first
await queryRunner.query(
`ALTER TABLE "${schemaName}"."${tableName}" ALTER COLUMN "${columnName}" DROP DEFAULT`,
);
// Convert column to text to remove enum dependency
await queryRunner.query(
`ALTER TABLE "${schemaName}"."${tableName}" ALTER COLUMN "${columnName}" TYPE text USING "${columnName}"::text`,
);
// Drop the old enum (now safe since no column uses it)
await queryRunner.query(
`DROP TYPE IF EXISTS ${schemaName}."${enumName}" CASCADE`,
);
// Create new enum with all MessageChannelSyncStage values
await queryRunner.query(
`CREATE TYPE ${schemaName}."${enumName}" AS ENUM (
'${MessageChannelSyncStage.PENDING_CONFIGURATION}',
'${MessageChannelSyncStage.MESSAGE_LIST_FETCH_PENDING}',
'${MessageChannelSyncStage.MESSAGE_LIST_FETCH_SCHEDULED}',
'${MessageChannelSyncStage.MESSAGE_LIST_FETCH_ONGOING}',
'${MessageChannelSyncStage.MESSAGES_IMPORT_PENDING}',
'${MessageChannelSyncStage.MESSAGES_IMPORT_SCHEDULED}',
'${MessageChannelSyncStage.MESSAGES_IMPORT_ONGOING}',
'${MessageChannelSyncStage.FAILED}'
)`,
);
// Convert column back to enum type
await queryRunner.query(
`ALTER TABLE "${schemaName}"."${tableName}"
ALTER COLUMN "${columnName}" TYPE ${schemaName}."${enumName}"
USING "${columnName}"::${schemaName}."${enumName}"`,
);
// Update default value to PENDING_CONFIGURATION
await queryRunner.query(
`ALTER TABLE "${schemaName}"."${tableName}"
ALTER COLUMN "${columnName}" SET DEFAULT '${MessageChannelSyncStage.PENDING_CONFIGURATION}'::${schemaName}."${enumName}"`,
);
await queryRunner.commitTransaction();
this.logger.log(
`Successfully replaced ${enumName} with complete MessageChannelSyncStage enum values and updated default to PENDING_CONFIGURATION for workspace ${workspaceId}`,
);
} catch (error) {
await queryRunner.rollbackTransaction();
this.logger.error(
`Error replacing ${enumName} for workspace ${workspaceId}: ${error}`,
);
throw error;
} finally {
await queryRunner.release();
}
}
private async updateMessageChannelSyncStageFieldMetadata(
workspaceId: string,
options: RunOnWorkspaceArgs['options'],
): Promise<void> {
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 field metadata update`,
);
return;
}
const syncStageField = await this.fieldMetadataRepository.findOne({
where: {
standardId: MESSAGE_CHANNEL_STANDARD_FIELD_IDS.syncStage,
@@ -75,90 +185,72 @@ export class AddMessagesImportScheduledSyncStageCommand extends ActiveOrSuspende
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}`,
`Would add MESSAGES_IMPORT_SCHEDULED to MessageChannel syncStage field metadata 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',
},
],
const syncStageFieldOptions = [
{
value: MessageChannelSyncStage.MESSAGE_LIST_FETCH_PENDING,
label: 'Messages list fetch pending',
position: 0,
color: 'blue',
},
workspaceId,
isSystemBuild: true,
});
{
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',
},
];
syncStageField.options = syncStageFieldOptions;
syncStageField.defaultValue = `'${MessageChannelSyncStage.PENDING_CONFIGURATION}'`;
await this.fieldMetadataRepository.save(syncStageField);
this.logger.log(
`Successfully added MESSAGES_IMPORT_SCHEDULED sync stage for workspace ${workspaceId}`,
`Added MESSAGES_IMPORT_SCHEDULED to MessageChannel syncStage field metadata for workspace ${workspaceId}`,
);
}
}