diff --git a/packages/twenty-front/src/modules/object-metadata/utils/getObjectMetadataItemsMock.ts b/packages/twenty-front/src/modules/object-metadata/utils/getObjectMetadataItemsMock.ts index 4102d3cc3b75..230d09c965aa 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/getObjectMetadataItemsMock.ts +++ b/packages/twenty-front/src/modules/object-metadata/utils/getObjectMetadataItemsMock.ts @@ -1711,20 +1711,20 @@ export const getObjectMetadataItemsMock = () => { "updatedAt": "2024-08-05T16:38:57.285Z", "fromRelationMetadata": null, "toRelationMetadata": null, - "defaultValue": "'incoming'", + "defaultValue": "'INCOMING'", "options": [ { "id": "14216544-33d1-47d0-99a9-717763d49c0f", "color": "green", "label": "Incoming", - "value": "incoming", + "value": "INCOMING", "position": 0 }, { "id": "f1b89e48-1107-45a2-b54a-31a75e76b9b2", "color": "blue", "label": "Outgoing", - "value": "outgoing", + "value": "OUTGOING", "position": 1 } ], diff --git a/packages/twenty-front/src/testing/mock-data/generated/standard-metadata-query-result.ts b/packages/twenty-front/src/testing/mock-data/generated/standard-metadata-query-result.ts index 250e1b6e6f1b..f94bbc2f7680 100644 --- a/packages/twenty-front/src/testing/mock-data/generated/standard-metadata-query-result.ts +++ b/packages/twenty-front/src/testing/mock-data/generated/standard-metadata-query-result.ts @@ -866,20 +866,20 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = "isNullable": false, "createdAt": "2024-08-02T16:00:05.938Z", "updatedAt": "2024-08-02T16:00:05.938Z", - "defaultValue": "'incoming'", + "defaultValue": "'INCOMING'", "options": [ { "id": "09fd3f5f-5903-4a3a-8f8b-335825349389", "color": "green", "label": "Incoming", - "value": "incoming", + "value": "INCOMING", "position": 0 }, { "id": "0df4272e-dfef-450e-84b7-d1477e66ee7f", "color": "blue", "label": "Outgoing", - "value": "outgoing", + "value": "OUTGOING", "position": 1 } ], diff --git a/packages/twenty-server/src/database/commands/database-command.module.ts b/packages/twenty-server/src/database/commands/database-command.module.ts index 6bf31eb73c2a..d240657f5f0f 100644 --- a/packages/twenty-server/src/database/commands/database-command.module.ts +++ b/packages/twenty-server/src/database/commands/database-command.module.ts @@ -7,8 +7,7 @@ import { DataSeedDemoWorkspaceCommand } from 'src/database/commands/data-seed-de import { DataSeedDemoWorkspaceModule } from 'src/database/commands/data-seed-demo-workspace/data-seed-demo-workspace.module'; import { DataSeedWorkspaceCommand } from 'src/database/commands/data-seed-dev-workspace.command'; import { ConfirmationQuestion } from 'src/database/commands/questions/confirmation.question'; -import { UpgradeTo0_23CommandModule } from 'src/database/commands/upgrade-version/0-23/0-23-upgrade-version.module'; -import { UpgradeVersionModule } from 'src/database/commands/upgrade-version/upgrade-version.module'; +import { UpgradeTo0_24CommandModule } from 'src/database/commands/upgrade-version/0-24/0-24-upgrade-version.module'; import { TypeORMModule } from 'src/database/typeorm/typeorm.module'; import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity'; import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; @@ -46,8 +45,7 @@ import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/worksp FieldMetadataModule, DataSeedDemoWorkspaceModule, WorkspaceMetadataVersionModule, - UpgradeTo0_23CommandModule, - UpgradeVersionModule, + UpgradeTo0_24CommandModule, ], providers: [ DataSeedWorkspaceCommand, diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-23/0-23-backfill-new-onboarding-user-vars.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-23/0-23-backfill-new-onboarding-user-vars.ts deleted file mode 100644 index ab1eafd4e6d1..000000000000 --- a/packages/twenty-server/src/database/commands/upgrade-version/0-23/0-23-backfill-new-onboarding-user-vars.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { Logger } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; - -import chalk from 'chalk'; -import { Command, CommandRunner, Option } from 'nest-commander'; -import { Repository } from 'typeorm'; - -import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service'; -import { - Workspace, - WorkspaceActivationStatus, -} from 'src/engine/core-modules/workspace/workspace.entity'; - -interface BackfillNewOnboardingUserVarsCommandOptions { - workspaceId?: string; -} - -@Command({ - name: 'upgrade-0.23:backfill-new-onboarding-user-vars', - description: 'Backfill new onboarding user vars for existing workspaces', -}) -export class BackfillNewOnboardingUserVarsCommand extends CommandRunner { - private readonly logger = new Logger( - BackfillNewOnboardingUserVarsCommand.name, - ); - constructor( - @InjectRepository(Workspace, 'core') - private readonly workspaceRepository: Repository, - private readonly onboardingService: OnboardingService, - ) { - super(); - } - - @Option({ - flags: '-w, --workspace-id [workspace_id]', - description: 'workspace id. Command runs on all workspaces if not provided', - required: false, - }) - parseWorkspaceId(value: string): string { - return value; - } - - async run( - _passedParam: string[], - options: BackfillNewOnboardingUserVarsCommandOptions, - ): Promise { - const workspaces = await this.workspaceRepository.find({ - where: { - activationStatus: WorkspaceActivationStatus.PENDING_CREATION, - ...(options.workspaceId && { id: options.workspaceId }), - }, - relations: ['users'], - }); - - if (!workspaces.length) { - this.logger.log(chalk.yellow('No workspace found')); - - return; - } - - this.logger.log( - chalk.green(`Running command on ${workspaces.length} workspaces`), - ); - - for (const workspace of workspaces) { - this.logger.log( - chalk.green(`Running command on workspace ${workspace.id}`), - ); - - await this.onboardingService.setOnboardingInviteTeamPending({ - workspaceId: workspace.id, - value: true, - }); - - for (const user of workspace.users) { - await this.onboardingService.setOnboardingCreateProfilePending({ - userId: user.id, - workspaceId: workspace.id, - value: true, - }); - - await this.onboardingService.setOnboardingConnectAccountPending({ - userId: user.id, - workspaceId: workspace.id, - value: true, - }); - } - } - - this.logger.log(chalk.green(`Command completed!`)); - } -} diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-23/0-23-migrate-domain-to-links.command.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-23/0-23-migrate-domain-to-links.command.ts deleted file mode 100644 index 422f97a1cefb..000000000000 --- a/packages/twenty-server/src/database/commands/upgrade-version/0-23/0-23-migrate-domain-to-links.command.ts +++ /dev/null @@ -1,308 +0,0 @@ -import { Logger } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; - -import chalk from 'chalk'; -import { Command, CommandRunner, Option } from 'nest-commander'; -import { QueryRunner, Repository } from 'typeorm'; - -import { TypeORMService } from 'src/database/typeorm/typeorm.service'; -import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity'; -import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; -import { CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input'; -import { - FieldMetadataEntity, - FieldMetadataType, -} from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata/field-metadata.service'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; -import { WorkspaceStatusService } from 'src/engine/workspace-manager/workspace-status/services/workspace-status.service'; -import { COMPANY_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids'; -import { ViewService } from 'src/modules/view/services/view.service'; -import { ViewFieldWorkspaceEntity } from 'src/modules/view/standard-objects/view-field.workspace-entity'; - -interface MigrateDomainNameFromTextToLinksCommandOptions { - workspaceId?: string; -} - -@Command({ - name: 'upgrade-0.23:migrate-domain-standard-field-to-links', - description: - 'Migrating field domainName of deprecated type TEXT to type LINKS', -}) -export class MigrateDomainNameFromTextToLinksCommand extends CommandRunner { - private readonly logger = new Logger( - MigrateDomainNameFromTextToLinksCommand.name, - ); - constructor( - @InjectRepository(FieldMetadataEntity, 'metadata') - private readonly fieldMetadataRepository: Repository, - private readonly fieldMetadataService: FieldMetadataService, - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, - private readonly typeORMService: TypeORMService, - private readonly dataSourceService: DataSourceService, - private readonly workspaceStatusService: WorkspaceStatusService, - private readonly viewService: ViewService, - ) { - super(); - } - - @Option({ - flags: '-w, --workspace-id [workspace_id]', - description: - 'workspace id. Command runs on all active workspaces if not provided', - required: false, - }) - parseWorkspaceId(value: string): string { - return value; - } - - async run( - _passedParam: string[], - options: MigrateDomainNameFromTextToLinksCommandOptions, - ): Promise { - this.logger.log( - 'Running command to migrate standard field domainName from text to Link', - ); - let workspaceIds: string[] = []; - - if (options.workspaceId) { - workspaceIds = [options.workspaceId]; - } else { - const activeWorkspaceIds = - await this.workspaceStatusService.getActiveWorkspaceIds(); - - workspaceIds = activeWorkspaceIds; - } - - if (!workspaceIds.length) { - this.logger.log(chalk.yellow('No workspace found')); - - return; - } else { - this.logger.log( - chalk.green(`Running command on ${workspaceIds.length} workspaces`), - ); - } - - for (const workspaceId of workspaceIds) { - this.logger.log(`Running command for workspace ${workspaceId}`); - try { - const dataSourceMetadata = - await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceId( - workspaceId, - ); - - if (!dataSourceMetadata) { - throw new Error( - `Could not find dataSourceMetadata for workspace ${workspaceId}`, - ); - } - - const workspaceDataSource = - await this.typeORMService.connectToDataSource(dataSourceMetadata); - - if (!workspaceDataSource) { - throw new Error( - `Could not connect to dataSource for workspace ${workspaceId}`, - ); - } - - const domainNameField = await this.fieldMetadataRepository.findOneBy({ - workspaceId, - standardId: COMPANY_STANDARD_FIELD_IDS.domainName, - }); - - if (!domainNameField) { - throw new Error('Could not find domainName field'); - } - - if (domainNameField.type === FieldMetadataType.LINKS) { - this.logger.log( - `Field domainName is already of type LINKS, skipping migration.`, - ); - continue; - } - - this.logger.log(`Attempting to migrate domainName field.`); - - const workspaceQueryRunner = workspaceDataSource.createQueryRunner(); - - await workspaceQueryRunner.connect(); - - const fieldName = domainNameField.name; - const { - id: _id, - createdAt: _createdAt, - updatedAt: _updatedAt, - ...domainNameFieldWithoutIdAndTimestamps - } = domainNameField; - - try { - const tmpNewDomainLinksField = - await this.fieldMetadataService.createOne({ - ...domainNameFieldWithoutIdAndTimestamps, - type: FieldMetadataType.LINKS, - name: `${fieldName}Tmp`, - defaultValue: { - primaryLinkUrl: domainNameField.defaultValue, - secondaryLinks: null, - primaryLinkLabel: "''", - }, - } satisfies CreateFieldInput); - - // Migrate data from domainName to primaryLinkUrl - await this.migrateDataWithinCompanyTable({ - sourceColumnName: `${domainNameField.name}`, - targetColumnName: `${tmpNewDomainLinksField.name}PrimaryLinkUrl`, - workspaceQueryRunner, - dataSourceMetadata, - }); - - // Duplicate initial domainName text field's views behaviour for new domainName field - await this.viewService.removeFieldFromViews({ - workspaceId: workspaceId, - fieldId: tmpNewDomainLinksField.id, - }); - - const viewFieldRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'viewField', - ); - const viewFieldsWithDeprecatedField = await viewFieldRepository.find({ - where: { - fieldMetadataId: domainNameField.id, - isVisible: true, - }, - }); - - await this.viewService.addFieldToViews({ - workspaceId: workspaceId, - fieldId: tmpNewDomainLinksField.id, - viewsIds: viewFieldsWithDeprecatedField - .filter((viewField) => viewField.viewId !== null) - .map((viewField) => viewField.viewId as string), - positions: viewFieldsWithDeprecatedField.reduce( - (acc, viewField) => { - if (!viewField.viewId) { - return acc; - } - acc[viewField.viewId] = viewField.position; - - return acc; - }, - [], - ), - size: 150, - }); - - // Delete initial domainName text field - await this.fieldMetadataService.deleteOneField( - { id: domainNameField.id }, - workspaceId, - ); - - // Rename temporary domainName links field - await this.fieldMetadataService.updateOne(tmpNewDomainLinksField.id, { - id: tmpNewDomainLinksField.id, - workspaceId: tmpNewDomainLinksField.workspaceId, - name: `${fieldName}`, - isCustom: false, - }); - - this.logger.log(`Migration of domainName done!`); - } catch (error) { - this.logger.log(`Error: ${error.message}`); - this.logger.log( - `Failed to migrate domainName ${domainNameField.id}, rolling back.`, - ); - - // Re-create initial field if it was deleted - const initialField = - await this.fieldMetadataService.findOneWithinWorkspace( - workspaceId, - { - where: { - name: `${domainNameField.name}`, - objectMetadataId: domainNameField.objectMetadataId, - }, - }, - ); - - const tmpNewDomainLinksField = - await this.fieldMetadataService.findOneWithinWorkspace( - workspaceId, - { - where: { - name: `${domainNameField.name}Tmp`, - objectMetadataId: domainNameField.objectMetadataId, - }, - }, - ); - - if (!initialField) { - this.logger.log(`Re-creating initial domainName field`); - const restoredField = await this.fieldMetadataService.createOne({ - ...domainNameField, - }); - - if (tmpNewDomainLinksField) { - this.logger.log(`Restoring data in domainName`); - await this.migrateDataWithinCompanyTable({ - sourceColumnName: `${tmpNewDomainLinksField.name}PrimaryLinkLabel`, - targetColumnName: `${restoredField.name}PrimaryLinkLabel`, - workspaceQueryRunner, - dataSourceMetadata, - }); - - await this.migrateDataWithinCompanyTable({ - sourceColumnName: `${tmpNewDomainLinksField.name}PrimaryLinkUrl`, - targetColumnName: `${restoredField.name}PrimaryLinkUrl`, - workspaceQueryRunner, - dataSourceMetadata, - }); - } else { - this.logger.log( - `Failed to restore data in domainName field ${domainNameField.id}`, - ); - } - } - - if (tmpNewDomainLinksField) { - await this.fieldMetadataService.deleteOneField( - { id: tmpNewDomainLinksField.id }, - workspaceId, - ); - } - } finally { - await workspaceQueryRunner.release(); - } - } catch (error) { - this.logger.log( - chalk.red( - `Running command on workspace ${workspaceId} failed with error: ${error}`, - ), - ); - continue; - } - - this.logger.log(chalk.green(`Command completed!`)); - } - } - - private async migrateDataWithinCompanyTable({ - sourceColumnName, - targetColumnName, - workspaceQueryRunner, - dataSourceMetadata, - }: { - sourceColumnName: string; - targetColumnName: string; - workspaceQueryRunner: QueryRunner; - dataSourceMetadata: DataSourceEntity; - }) { - await workspaceQueryRunner.query( - `UPDATE "${dataSourceMetadata.schema}"."company" SET "${targetColumnName}" = CASE WHEN "${sourceColumnName}" IS NULL OR "${sourceColumnName}" = '' THEN "${sourceColumnName}" WHEN "${sourceColumnName}" LIKE 'http%' THEN "${sourceColumnName}" ELSE 'https://' || "${sourceColumnName}" END;`, - ); - } -} diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-23/0-23-migrate-link-fields-to-links.command.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-23/0-23-migrate-link-fields-to-links.command.ts deleted file mode 100644 index a251388ec4f7..000000000000 --- a/packages/twenty-server/src/database/commands/upgrade-version/0-23/0-23-migrate-link-fields-to-links.command.ts +++ /dev/null @@ -1,342 +0,0 @@ -import { Logger } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; - -import chalk from 'chalk'; -import { Command, CommandRunner, Option } from 'nest-commander'; -import { QueryRunner, Repository } from 'typeorm'; - -import { TypeORMService } from 'src/database/typeorm/typeorm.service'; -import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity'; -import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; -import { CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input'; -import { FieldMetadataDefaultValueLink } from 'src/engine/metadata-modules/field-metadata/dtos/default-value.input'; -import { - FieldMetadataEntity, - FieldMetadataType, -} from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata/field-metadata.service'; -import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; -import { computeTableName } from 'src/engine/utils/compute-table-name.util'; -import { WorkspaceStatusService } from 'src/engine/workspace-manager/workspace-status/services/workspace-status.service'; -import { ViewService } from 'src/modules/view/services/view.service'; -import { ViewFieldWorkspaceEntity } from 'src/modules/view/standard-objects/view-field.workspace-entity'; - -interface MigrateLinkFieldsToLinksCommandOptions { - workspaceId?: string; -} - -@Command({ - name: 'upgrade-0.23:migrate-link-fields-to-links', - description: 'Migrating fields of deprecated type LINK to type LINKS', -}) -export class MigrateLinkFieldsToLinksCommand extends CommandRunner { - private readonly logger = new Logger(MigrateLinkFieldsToLinksCommand.name); - constructor( - @InjectRepository(FieldMetadataEntity, 'metadata') - private readonly fieldMetadataRepository: Repository, - @InjectRepository(ObjectMetadataEntity, 'metadata') - private readonly objectMetadataRepository: Repository, - private readonly fieldMetadataService: FieldMetadataService, - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, - private readonly typeORMService: TypeORMService, - private readonly dataSourceService: DataSourceService, - private readonly workspaceStatusService: WorkspaceStatusService, - private readonly viewService: ViewService, - ) { - super(); - } - - @Option({ - flags: '-w, --workspace-id [workspace_id]', - description: - 'workspace id. Command runs on all active workspaces if not provided', - required: false, - }) - parseWorkspaceId(value: string): string { - return value; - } - - async run( - _passedParam: string[], - options: MigrateLinkFieldsToLinksCommandOptions, - ): Promise { - this.logger.log( - 'Running command to migrate link type fields to links type', - ); - let workspaceIds: string[] = []; - - if (options.workspaceId) { - workspaceIds = [options.workspaceId]; - } else { - const activeWorkspaceIds = - await this.workspaceStatusService.getActiveWorkspaceIds(); - - workspaceIds = activeWorkspaceIds; - } - - if (!workspaceIds.length) { - this.logger.log(chalk.yellow('No workspace found')); - - return; - } else { - this.logger.log( - chalk.green(`Running command on ${workspaceIds.length} workspaces`), - ); - } - - for (const workspaceId of workspaceIds) { - this.logger.log(`Running command for workspace ${workspaceId}`); - try { - const dataSourceMetadata = - await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceId( - workspaceId, - ); - - if (!dataSourceMetadata) { - throw new Error( - `Could not find dataSourceMetadata for workspace ${workspaceId}`, - ); - } - - const workspaceDataSource = - await this.typeORMService.connectToDataSource(dataSourceMetadata); - - if (!workspaceDataSource) { - throw new Error( - `Could not connect to dataSource for workspace ${workspaceId}`, - ); - } - - const fieldsWithLinkType = await this.fieldMetadataRepository.find({ - where: { - workspaceId, - type: FieldMetadataType.LINK, - }, - }); - - for (const fieldWithLinkType of fieldsWithLinkType) { - const objectMetadata = await this.objectMetadataRepository.findOne({ - where: { id: fieldWithLinkType.objectMetadataId }, - }); - - if (!objectMetadata) { - throw new Error( - `Could not find objectMetadata for field ${fieldWithLinkType.name}`, - ); - } - - this.logger.log( - `Attempting to migrate field ${fieldWithLinkType.name} on ${objectMetadata.nameSingular}.`, - ); - const workspaceQueryRunner = workspaceDataSource.createQueryRunner(); - - await workspaceQueryRunner.connect(); - - const fieldName = fieldWithLinkType.name; - const { id: _id, ...fieldWithLinkTypeWithoutId } = fieldWithLinkType; - - const linkDefaultValue = - fieldWithLinkTypeWithoutId.defaultValue as FieldMetadataDefaultValueLink; - - const defaultValueForLinksField = { - primaryLinkUrl: linkDefaultValue.url, - primaryLinkLabel: linkDefaultValue.label, - secondaryLinks: null, - }; - - try { - const tmpNewLinksField = await this.fieldMetadataService.createOne({ - ...fieldWithLinkTypeWithoutId, - type: FieldMetadataType.LINKS, - defaultValue: defaultValueForLinksField, - name: `${fieldName}Tmp`, - } satisfies CreateFieldInput); - - const tableName = computeTableName( - objectMetadata.nameSingular, - objectMetadata.isCustom, - ); - - // Migrate data from linkLabel to primaryLinkLabel - await this.migrateDataWithinTable({ - sourceColumnName: `${fieldWithLinkType.name}Label`, - targetColumnName: `${tmpNewLinksField.name}PrimaryLinkLabel`, - tableName, - workspaceQueryRunner, - dataSourceMetadata, - }); - - // Migrate data from linkUrl to primaryLinkUrl - await this.migrateDataWithinTable({ - sourceColumnName: `${fieldWithLinkType.name}Url`, - targetColumnName: `${tmpNewLinksField.name}PrimaryLinkUrl`, - tableName, - workspaceQueryRunner, - dataSourceMetadata, - }); - - // Duplicate link field's views behaviour for new links field - await this.viewService.removeFieldFromViews({ - workspaceId: workspaceId, - fieldId: tmpNewLinksField.id, - }); - - const viewFieldRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'viewField', - ); - const viewFieldsWithDeprecatedField = - await viewFieldRepository.find({ - where: { - fieldMetadataId: fieldWithLinkType.id, - isVisible: true, - }, - }); - - await this.viewService.addFieldToViews({ - workspaceId: workspaceId, - fieldId: tmpNewLinksField.id, - viewsIds: viewFieldsWithDeprecatedField - .filter((viewField) => viewField.viewId !== null) - .map((viewField) => viewField.viewId as string), - positions: viewFieldsWithDeprecatedField.reduce( - (acc, viewField) => { - if (!viewField.viewId) { - return acc; - } - acc[viewField.viewId] = viewField.position; - - return acc; - }, - [], - ), - }); - - // Delete link field - await this.fieldMetadataService.deleteOneField( - { id: fieldWithLinkType.id }, - workspaceId, - ); - - // Rename temporary links field - await this.fieldMetadataService.updateOne(tmpNewLinksField.id, { - id: tmpNewLinksField.id, - workspaceId: tmpNewLinksField.workspaceId, - name: `${fieldName}`, - isCustom: false, - }); - - this.logger.log( - `Migration of ${fieldWithLinkType.name} on ${objectMetadata.nameSingular} done!`, - ); - } catch (error) { - this.logger.log( - `Failed to migrate field ${fieldWithLinkType.name} on ${objectMetadata.nameSingular}, rolling back.`, - ); - - // Re-create initial field if it was deleted - const initialField = - await this.fieldMetadataService.findOneWithinWorkspace( - workspaceId, - { - where: { - name: `${fieldWithLinkType.name}`, - objectMetadataId: fieldWithLinkType.objectMetadataId, - }, - }, - ); - - const tmpNewLinksField = - await this.fieldMetadataService.findOneWithinWorkspace( - workspaceId, - { - where: { - name: `${fieldWithLinkType.name}Tmp`, - objectMetadataId: fieldWithLinkType.objectMetadataId, - }, - }, - ); - - if (!initialField) { - this.logger.log( - `Re-creating initial link field ${fieldWithLinkType.name} but of type links`, // Cannot create link fields anymore - ); - const restoredField = await this.fieldMetadataService.createOne({ - ...fieldWithLinkType, - defaultValue: defaultValueForLinksField, - type: FieldMetadataType.LINKS, - }); - const tableName = computeTableName( - objectMetadata.nameSingular, - objectMetadata.isCustom, - ); - - if (tmpNewLinksField) { - this.logger.log( - `Restoring data in field ${fieldWithLinkType.name}`, - ); - await this.migrateDataWithinTable({ - sourceColumnName: `${tmpNewLinksField.name}PrimaryLinkLabel`, - targetColumnName: `${restoredField.name}PrimaryLinkLabel`, - tableName, - workspaceQueryRunner, - dataSourceMetadata, - }); - - await this.migrateDataWithinTable({ - sourceColumnName: `${tmpNewLinksField.name}PrimaryLinkUrl`, - targetColumnName: `${restoredField.name}PrimaryLinkUrl`, - tableName, - workspaceQueryRunner, - dataSourceMetadata, - }); - } else { - this.logger.log( - `Failed to restore data in link field ${fieldWithLinkType.name}`, - ); - } - } - - if (tmpNewLinksField) { - await this.fieldMetadataService.deleteOneField( - { id: tmpNewLinksField.id }, - workspaceId, - ); - } - } finally { - await workspaceQueryRunner.release(); - } - } - } catch (error) { - this.logger.log( - chalk.red( - `Running command on workspace ${workspaceId} failed with error: ${error}`, - ), - ); - continue; - } - - this.logger.log(chalk.green(`Command completed!`)); - } - } - - private async migrateDataWithinTable({ - sourceColumnName, - targetColumnName, - tableName, - workspaceQueryRunner, - dataSourceMetadata, - }: { - sourceColumnName: string; - targetColumnName: string; - tableName: string; - workspaceQueryRunner: QueryRunner; - dataSourceMetadata: DataSourceEntity; - }) { - await workspaceQueryRunner.query( - `UPDATE "${dataSourceMetadata.schema}"."${tableName}" SET "${targetColumnName}" = "${sourceColumnName}"`, - ); - } -} diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-23/0-23-migrate-message-channel-sync-status-enum.command.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-23/0-23-migrate-message-channel-sync-status-enum.command.ts deleted file mode 100644 index 5e31d61c2b50..000000000000 --- a/packages/twenty-server/src/database/commands/upgrade-version/0-23/0-23-migrate-message-channel-sync-status-enum.command.ts +++ /dev/null @@ -1,231 +0,0 @@ -import { Logger } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; - -import chalk from 'chalk'; -import { Command, CommandRunner, Option } from 'nest-commander'; -import { Repository } from 'typeorm'; -import { v4 } from 'uuid'; - -import { TypeORMService } from 'src/database/typeorm/typeorm.service'; -import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; -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 { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.service'; -import { WorkspaceStatusService } from 'src/engine/workspace-manager/workspace-status/services/workspace-status.service'; -import { MessageChannelSyncStatus } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; - -interface MigrateMessageChannelSyncStatusEnumCommandOptions { - workspaceId?: string; -} - -@Command({ - name: 'upgrade-0.23:update-message-channel-sync-status-enum', - description: 'Migrate messageChannel syncStatus enum', -}) -export class MigrateMessageChannelSyncStatusEnumCommand extends CommandRunner { - private readonly logger = new Logger( - MigrateMessageChannelSyncStatusEnumCommand.name, - ); - constructor( - private readonly workspaceStatusService: WorkspaceStatusService, - @InjectRepository(FieldMetadataEntity, 'metadata') - private readonly fieldMetadataRepository: Repository, - @InjectRepository(ObjectMetadataEntity, 'metadata') - private readonly objectMetadataRepository: Repository, - private readonly typeORMService: TypeORMService, - private readonly dataSourceService: DataSourceService, - private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService, - ) { - super(); - } - - @Option({ - flags: '-w, --workspace-id [workspace_id]', - description: 'workspace id. Command runs on all workspaces if not provided', - required: false, - }) - parseWorkspaceId(value: string): string { - return value; - } - - async run( - _passedParam: string[], - options: MigrateMessageChannelSyncStatusEnumCommandOptions, - ): Promise { - let workspaceIds: string[] = []; - - if (options.workspaceId) { - workspaceIds = [options.workspaceId]; - } else { - workspaceIds = await this.workspaceStatusService.getActiveWorkspaceIds(); - } - - if (!workspaceIds.length) { - this.logger.log(chalk.yellow('No workspace found')); - - return; - } else { - this.logger.log( - chalk.green(`Running command on ${workspaceIds.length} workspaces`), - ); - } - - for (const workspaceId of workspaceIds) { - try { - const dataSourceMetadatas = - await this.dataSourceService.getDataSourcesMetadataFromWorkspaceId( - workspaceId, - ); - - for (const dataSourceMetadata of dataSourceMetadatas) { - const workspaceDataSource = - await this.typeORMService.connectToDataSource(dataSourceMetadata); - - if (workspaceDataSource) { - const queryRunner = workspaceDataSource.createQueryRunner(); - - await queryRunner.connect(); - await queryRunner.startTransaction(); - - try { - await queryRunner.query( - `ALTER TYPE "${dataSourceMetadata.schema}"."messageChannel_syncStatus_enum" RENAME TO "messageChannel_syncStatus_enum_old"`, - ); - await queryRunner.query( - `CREATE TYPE "${dataSourceMetadata.schema}"."messageChannel_syncStatus_enum" AS ENUM ( - 'ONGOING', - 'NOT_SYNCED', - 'ACTIVE', - 'FAILED_INSUFFICIENT_PERMISSIONS', - 'FAILED_UNKNOWN' - )`, - ); - - await queryRunner.query( - `ALTER TABLE "${dataSourceMetadata.schema}"."messageChannel" ALTER COLUMN "syncStatus" DROP DEFAULT`, - ); - await queryRunner.query( - `ALTER TABLE "${dataSourceMetadata.schema}"."messageChannel" ALTER COLUMN "syncStatus" TYPE text`, - ); - - await queryRunner.query( - `UPDATE "${dataSourceMetadata.schema}"."messageChannel" SET "syncStatus" = 'ACTIVE' WHERE "syncStatus" = 'COMPLETED'`, - ); - - await queryRunner.query( - `ALTER TABLE "${dataSourceMetadata.schema}"."messageChannel" ALTER COLUMN "syncStatus" TYPE "${dataSourceMetadata.schema}"."messageChannel_syncStatus_enum" USING "syncStatus"::text::"${dataSourceMetadata.schema}"."messageChannel_syncStatus_enum"`, - ); - - await queryRunner.query( - `ALTER TABLE "${dataSourceMetadata.schema}"."messageChannel" ALTER COLUMN "syncStatus" SET DEFAULT NULL`, - ); - - await queryRunner.query( - `DROP TYPE "${dataSourceMetadata.schema}"."messageChannel_syncStatus_enum_old"`, - ); - await queryRunner.commitTransaction(); - } catch (error) { - await queryRunner.rollbackTransaction(); - this.logger.log( - chalk.red(`Running command on workspace ${workspaceId} failed`), - ); - throw error; - } finally { - await queryRunner.release(); - } - } - } - - const messageChannelObjectMetadata = - await this.objectMetadataRepository.findOne({ - where: { nameSingular: 'messageChannel', workspaceId }, - }); - - if (!messageChannelObjectMetadata) { - this.logger.log( - chalk.yellow( - `Object metadata for messageChannel not found in workspace ${workspaceId}`, - ), - ); - - continue; - } - - const syncStatusFieldMetadata = - await this.fieldMetadataRepository.findOne({ - where: { - name: 'syncStatus', - workspaceId, - objectMetadataId: messageChannelObjectMetadata.id, - }, - }); - - if (!syncStatusFieldMetadata) { - this.logger.log( - chalk.yellow( - `Field metadata for syncStatus not found in workspace ${workspaceId}`, - ), - ); - - continue; - } - - const newOptions = [ - { - id: v4(), - value: MessageChannelSyncStatus.ONGOING, - label: 'Ongoing', - position: 1, - color: 'yellow', - }, - { - id: v4(), - value: MessageChannelSyncStatus.NOT_SYNCED, - label: 'Not Synced', - position: 2, - color: 'blue', - }, - { - id: v4(), - value: MessageChannelSyncStatus.ACTIVE, - label: 'Active', - position: 3, - color: 'green', - }, - { - id: v4(), - value: MessageChannelSyncStatus.FAILED_INSUFFICIENT_PERMISSIONS, - label: 'Failed Insufficient Permissions', - position: 4, - color: 'red', - }, - { - id: v4(), - value: MessageChannelSyncStatus.FAILED_UNKNOWN, - label: 'Failed Unknown', - position: 5, - color: 'red', - }, - ]; - - await this.fieldMetadataRepository.update(syncStatusFieldMetadata.id, { - options: newOptions, - }); - - await this.workspaceMetadataVersionService.incrementMetadataVersion( - workspaceId, - ); - - this.logger.log( - chalk.green(`Running command on workspace ${workspaceId} done`), - ); - } catch (error) { - this.logger.error( - `Migration failed for workspace ${workspaceId}: ${error.message}`, - ); - } - } - - this.logger.log(chalk.green(`Command completed!`)); - } -} diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-23/0-23-set-user-vars-accounts-to-reconnect.command.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-23/0-23-set-user-vars-accounts-to-reconnect.command.ts deleted file mode 100644 index 726d698eac38..000000000000 --- a/packages/twenty-server/src/database/commands/upgrade-version/0-23/0-23-set-user-vars-accounts-to-reconnect.command.ts +++ /dev/null @@ -1,168 +0,0 @@ -import { Logger } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; - -import chalk from 'chalk'; -import { Command, CommandRunner, Option } from 'nest-commander'; -import { Repository } from 'typeorm'; - -import { - KeyValuePair, - KeyValuePairType, -} from 'src/engine/core-modules/key-value-pair/key-value-pair.entity'; -import { - Workspace, - WorkspaceActivationStatus, -} from 'src/engine/core-modules/workspace/workspace.entity'; -import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.service'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; -import { CalendarChannelSyncStatus } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity'; -import { AccountsToReconnectService } from 'src/modules/connected-account/services/accounts-to-reconnect.service'; -import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; -import { AccountsToReconnectKeys } from 'src/modules/connected-account/types/accounts-to-reconnect-key-value.type'; -import { MessageChannelSyncStatus } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; - -interface SetUserVarsAccountsToReconnectCommandOptions { - workspaceId?: string; -} - -@Command({ - name: 'upgrade-0.23:set-user-vars-accounts-to-reconnect', - description: 'Set user vars accounts to reconnect', -}) -export class SetUserVarsAccountsToReconnectCommand extends CommandRunner { - private readonly logger = new Logger( - SetUserVarsAccountsToReconnectCommand.name, - ); - constructor( - private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService, - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, - private readonly accountsToReconnectService: AccountsToReconnectService, - @InjectRepository(KeyValuePair, 'core') - private readonly keyValuePairRepository: Repository, - @InjectRepository(Workspace, 'core') - private readonly workspaceRepository: Repository, - ) { - super(); - } - - @Option({ - flags: '-w, --workspace-id [workspace_id]', - description: 'workspace id. Command runs on all workspaces if not provided', - required: false, - }) - parseWorkspaceId(value: string): string { - return value; - } - - async run( - _passedParam: string[], - options: SetUserVarsAccountsToReconnectCommandOptions, - ): Promise { - let activeWorkspaceIds: string[] = []; - - if (options.workspaceId) { - activeWorkspaceIds = [options.workspaceId]; - } else { - const activeWorkspaces = await this.workspaceRepository.find({ - where: { - activationStatus: WorkspaceActivationStatus.ACTIVE, - ...(options.workspaceId && { id: options.workspaceId }), - }, - }); - - activeWorkspaceIds = activeWorkspaces.map((workspace) => workspace.id); - } - - if (!activeWorkspaceIds.length) { - this.logger.log(chalk.yellow('No workspace found')); - - return; - } else { - this.logger.log( - chalk.green( - `Running command on ${activeWorkspaceIds.length} workspaces`, - ), - ); - } - - // Remove all deprecated user vars - await this.keyValuePairRepository.delete({ - type: KeyValuePairType.USER_VAR, - key: 'ACCOUNTS_TO_RECONNECT', - }); - - for (const workspaceId of activeWorkspaceIds) { - try { - const connectedAccountRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'connectedAccount', - ); - - try { - const connectedAccountsInFailedInsufficientPermissions = - await connectedAccountRepository.find({ - select: { - id: true, - accountOwner: { - userId: true, - }, - }, - where: [ - { - messageChannels: { - syncStatus: - MessageChannelSyncStatus.FAILED_INSUFFICIENT_PERMISSIONS, - }, - }, - { - calendarChannels: { - syncStatus: - CalendarChannelSyncStatus.FAILED_INSUFFICIENT_PERMISSIONS, - }, - }, - ], - relations: { - accountOwner: true, - }, - }); - - for (const connectedAccount of connectedAccountsInFailedInsufficientPermissions) { - try { - await this.accountsToReconnectService.addAccountToReconnectByKey( - AccountsToReconnectKeys.ACCOUNTS_TO_RECONNECT_INSUFFICIENT_PERMISSIONS, - connectedAccount.accountOwner.userId, - workspaceId, - connectedAccount.id, - ); - } catch (error) { - this.logger.error( - `Failed to add account to reconnect for workspace ${workspaceId}: ${error.message}`, - ); - throw error; - } - } - } catch (error) { - this.logger.log( - chalk.red(`Running command on workspace ${workspaceId} failed`), - ); - throw error; - } - - await this.workspaceMetadataVersionService.incrementMetadataVersion( - workspaceId, - ); - - this.logger.log( - chalk.green(`Running command on workspace ${workspaceId} done`), - ); - } catch (error) { - this.logger.error( - `Migration failed for workspace ${workspaceId}: ${error.message}`, - ); - } - } - - this.logger.log(chalk.green(`Command completed!`)); - } -} diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-23/0-23-set-workspace-activation-status.command.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-23/0-23-set-workspace-activation-status.command.ts deleted file mode 100644 index 8021a41830d3..000000000000 --- a/packages/twenty-server/src/database/commands/upgrade-version/0-23/0-23-set-workspace-activation-status.command.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { Logger } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; - -import chalk from 'chalk'; -import { Command, CommandRunner, Option } from 'nest-commander'; -import { Repository } from 'typeorm'; - -import { TypeORMService } from 'src/database/typeorm/typeorm.service'; -import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service'; -import { - Workspace, - WorkspaceActivationStatus, -} from 'src/engine/core-modules/workspace/workspace.entity'; -import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; -import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.service'; - -interface SetWorkspaceActivationStatusCommandOptions { - workspaceId?: string; -} - -@Command({ - name: 'upgrade-0.23:set-workspace-activation-status', - description: 'Set workspace activation status', -}) -export class SetWorkspaceActivationStatusCommand extends CommandRunner { - private readonly logger = new Logger( - SetWorkspaceActivationStatusCommand.name, - ); - constructor( - @InjectRepository(Workspace, 'core') - private readonly workspaceRepository: Repository, - private readonly typeORMService: TypeORMService, - private readonly dataSourceService: DataSourceService, - private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService, - private readonly billingSubscriptionService: BillingSubscriptionService, - ) { - super(); - } - - @Option({ - flags: '-w, --workspace-id [workspace_id]', - description: 'workspace id. Command runs on all workspaces if not provided', - required: false, - }) - parseWorkspaceId(value: string): string { - return value; - } - - async run( - _passedParam: string[], - options: SetWorkspaceActivationStatusCommandOptions, - ): Promise { - let activeSubscriptionWorkspaceIds: string[] = []; - - if (options.workspaceId) { - activeSubscriptionWorkspaceIds = [options.workspaceId]; - } else { - activeSubscriptionWorkspaceIds = - await this.billingSubscriptionService.getActiveSubscriptionWorkspaceIds(); - } - - if (!activeSubscriptionWorkspaceIds.length) { - this.logger.log(chalk.yellow('No workspace found')); - - return; - } else { - this.logger.log( - chalk.green( - `Running command on ${activeSubscriptionWorkspaceIds.length} workspaces`, - ), - ); - } - - for (const workspaceId of activeSubscriptionWorkspaceIds) { - try { - const dataSourceMetadatas = - await this.dataSourceService.getDataSourcesMetadataFromWorkspaceId( - workspaceId, - ); - - for (const dataSourceMetadata of dataSourceMetadatas) { - const workspaceDataSource = - await this.typeORMService.connectToDataSource(dataSourceMetadata); - - if (workspaceDataSource) { - try { - await this.workspaceRepository.update( - { id: workspaceId }, - { activationStatus: WorkspaceActivationStatus.ACTIVE }, - ); - } catch (error) { - this.logger.log( - chalk.red(`Running command on workspace ${workspaceId} failed`), - ); - throw error; - } - } - } - - await this.workspaceMetadataVersionService.incrementMetadataVersion( - workspaceId, - ); - - this.logger.log( - chalk.green(`Running command on workspace ${workspaceId} done`), - ); - } catch (error) { - this.logger.error( - `Migration failed for workspace ${workspaceId}: ${error.message}`, - ); - } - } - - this.logger.log(chalk.green(`Command completed!`)); - } -} diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-23/0-23-update-activities.command.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-23/0-23-update-activities.command.ts deleted file mode 100644 index da4db435a295..000000000000 --- a/packages/twenty-server/src/database/commands/upgrade-version/0-23/0-23-update-activities.command.ts +++ /dev/null @@ -1,494 +0,0 @@ -import { Logger } from '@nestjs/common'; - -import chalk from 'chalk'; -import { Command, CommandRunner, Option } from 'nest-commander'; -import { QueryRunner } from 'typeorm'; -import { v4 } from 'uuid'; - -import { TypeORMService } from 'src/database/typeorm/typeorm.service'; -import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; -import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service'; -import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.service'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; -import { notesAllView } from 'src/engine/workspace-manager/standard-objects-prefill-data/views/notes-all.view'; -import { tasksAllView } from 'src/engine/workspace-manager/standard-objects-prefill-data/views/tasks-all.view'; -import { tasksByStatusView } from 'src/engine/workspace-manager/standard-objects-prefill-data/views/tasks-by-status.view'; -import { WorkspaceStatusService } from 'src/engine/workspace-manager/workspace-status/services/workspace-status.service'; -import { ActivityWorkspaceEntity } from 'src/modules/activity/standard-objects/activity.workspace-entity'; -import { AttachmentWorkspaceEntity } from 'src/modules/attachment/standard-objects/attachment.workspace-entity'; -import { NoteTargetWorkspaceEntity } from 'src/modules/note/standard-objects/note-target.workspace-entity'; -import { NoteWorkspaceEntity } from 'src/modules/note/standard-objects/note.workspace-entity'; -import { TaskTargetWorkspaceEntity } from 'src/modules/task/standard-objects/task-target.workspace-entity'; -import { TaskWorkspaceEntity } from 'src/modules/task/standard-objects/task.workspace-entity'; -import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-objects/timeline-activity.workspace-entity'; - -interface UpdateActivitiesCommandOptions { - workspaceId?: string; -} - -type CoreLogicFunction = (params: { - workspaceId: string; - queryRunner?: QueryRunner; - schema?: string; -}) => Promise; - -@Command({ - name: 'upgrade-0.23:update-activities-type', - description: 'Migrate Activity object to Note and Task objects', -}) -export class UpdateActivitiesCommand extends CommandRunner { - private readonly logger = new Logger(UpdateActivitiesCommand.name); - - constructor( - private readonly workspaceStatusService: WorkspaceStatusService, - private readonly typeORMService: TypeORMService, - private readonly dataSourceService: DataSourceService, - private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService, - private readonly objectMetadataService: ObjectMetadataService, - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, - ) { - super(); - } - - @Option({ - flags: '-w, --workspace-id [workspace_id]', - description: 'workspace id. Command runs on all workspaces if not provided', - required: false, - }) - parseWorkspaceId(value: string): string { - return value; - } - - async run( - _passedParam: string[], - options: UpdateActivitiesCommandOptions, - ): Promise { - const updateActivities = async ({ - workspaceId, - queryRunner, - schema, - }: { - workspaceId: string; - queryRunner: QueryRunner; - schema: string; - }): Promise => { - /*********************** - // Transfer Activities to NOTE + Tasks - ***********************/ - - const activityRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'activity', - ); - const noteRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'note', - ); - const noteTargetRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'noteTarget', - ); - const taskRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'task', - ); - const taskTargetRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'taskTarget', - ); - const timelineActivityRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'timelineActivity', - ); - const attachmentRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'attachment', - ); - - const objectMetadata = - await this.objectMetadataService.findManyWithinWorkspace(workspaceId); - - const noteObjectMetadataId = objectMetadata.find( - (object) => object.nameSingular === 'note', - )?.id; - - const taskObjectMetadataId = objectMetadata.find( - (object) => object.nameSingular === 'task', - )?.id; - - const activityObjectMetadataId = objectMetadata.find( - (object) => object.nameSingular === 'activity', - )?.id; - - const activitiesToTransfer = await activityRepository.find({ - order: { createdAt: 'ASC' }, - relations: ['activityTargets'], - }); - - for (let i = 0; i < activitiesToTransfer.length; i++) { - const activity = activitiesToTransfer[i]; - - if (activity.type === 'Note') { - const note = noteRepository.create({ - id: activity.id, - title: activity.title, - body: activity.body, - createdAt: activity.createdAt, - updatedAt: activity.updatedAt, - position: i, - }); - - await noteRepository.save(note); - - if (activity.activityTargets && activity.activityTargets.length > 0) { - const noteTargets = activity.activityTargets.map( - (activityTarget) => { - const { activityId, ...activityTargetData } = activityTarget; - - return noteTargetRepository.create({ - noteId: activityId, - ...activityTargetData, - }); - }, - ); - - await noteTargetRepository.save(noteTargets); - } - - await timelineActivityRepository.update( - { - name: 'note.created', - linkedObjectMetadataId: activityObjectMetadataId, - linkedRecordId: activity.id, - }, - { - linkedObjectMetadataId: noteObjectMetadataId, - name: 'linked-note.created', - }, - ); - - await timelineActivityRepository.update( - { - name: 'note.updated', - linkedObjectMetadataId: activityObjectMetadataId, - linkedRecordId: activity.id, - }, - { - linkedObjectMetadataId: noteObjectMetadataId, - name: 'linked-note.updated', - }, - ); - - await attachmentRepository.update( - { - activityId: activity.id, - }, - { - activityId: null, - noteId: activity.id, - }, - ); - } else if (activity.type === 'Task') { - const task = taskRepository.create({ - id: activity.id, - title: activity.title, - body: activity.body, - status: activity.completedAt ? 'DONE' : 'TODO', - dueAt: activity.dueAt, - assigneeId: activity.assigneeId, - position: i, - createdAt: activity.createdAt, - updatedAt: activity.updatedAt, - }); - - await taskRepository.save(task); - - if (activity.activityTargets && activity.activityTargets.length > 0) { - const taskTargets = activity.activityTargets.map( - (activityTarget) => { - const { activityId, ...activityTargetData } = activityTarget; - - return taskTargetRepository.create({ - taskId: activityId, - ...activityTargetData, - }); - }, - ); - - await taskTargetRepository.save(taskTargets); - } - - await timelineActivityRepository.update( - { - name: 'task.created', - linkedObjectMetadataId: activityObjectMetadataId, - linkedRecordId: activity.id, - }, - { - linkedObjectMetadataId: taskObjectMetadataId, - name: 'linked-task.created', - }, - ); - - await timelineActivityRepository.update( - { - name: 'task.updated', - linkedObjectMetadataId: activityObjectMetadataId, - linkedRecordId: activity.id, - }, - { - linkedObjectMetadataId: taskObjectMetadataId, - name: 'linked-task.updated', - }, - ); - await attachmentRepository.update( - { - activityId: activity.id, - }, - { - activityId: null, - taskId: activity.id, - }, - ); - } else { - throw new Error(`Unknown activity type: ${activity.type}`); - } - } - - // Hack to make sure the command is indempotent and return if one of the view exists - const viewExists = await queryRunner.manager - .createQueryBuilder() - .select() - .from(`${schema}.view`, 'view') - .where('name = :name', { name: 'All Notes' }) - .getRawOne(); - - if (!viewExists) { - await this.createViews( - objectMetadata, - queryRunner, - schema, - workspaceId, - ); - } - }; - - return this.sharedBoilerplate(_passedParam, options, updateActivities); - } - - private async createViews( - objectMetadata: ObjectMetadataEntity[], - queryRunner: QueryRunner, - schema: string, - workspaceId: string, - ) { - const objectMetadataMap = objectMetadata.reduce((acc, object) => { - acc[object.standardId ?? ''] = { - id: object.id, - fields: object.fields.reduce((acc, field) => { - acc[field.standardId ?? ''] = field.id; - - return acc; - }, {}), - }; - - return acc; - }, {}) as Record; - - const viewDefinitions = [ - await notesAllView(objectMetadataMap), - await tasksAllView(objectMetadataMap), - await tasksByStatusView(objectMetadataMap), - ]; - - const viewDefinitionsWithId = viewDefinitions.map((viewDefinition) => ({ - ...viewDefinition, - id: v4(), - })); - - await queryRunner.manager - .createQueryBuilder() - .insert() - .into(`${schema}.view`, [ - 'id', - 'name', - 'objectMetadataId', - 'type', - 'key', - 'position', - 'icon', - 'kanbanFieldMetadataId', - ]) - .values( - viewDefinitionsWithId.map( - ({ - id, - name, - objectMetadataId, - type, - key, - position, - icon, - kanbanFieldMetadataId, - }) => ({ - id, - name, - objectMetadataId, - type, - key, - position, - icon, - kanbanFieldMetadataId, - }), - ), - ) - .returning('*') - .execute(); - - for (const viewDefinition of viewDefinitionsWithId) { - if (viewDefinition.fields && viewDefinition.fields.length > 0) { - await queryRunner.manager - .createQueryBuilder() - .insert() - .into(`${schema}.viewField`, [ - 'fieldMetadataId', - 'position', - 'isVisible', - 'size', - 'viewId', - ]) - .values( - viewDefinition.fields.map((field) => ({ - fieldMetadataId: field.fieldMetadataId, - position: field.position, - isVisible: field.isVisible, - size: field.size, - viewId: viewDefinition.id, - })), - ) - .execute(); - } - - if (viewDefinition.filters && viewDefinition.filters.length > 0) { - await queryRunner.manager - .createQueryBuilder() - .insert() - .into(`${schema}.viewFilter`, [ - 'fieldMetadataId', - 'displayValue', - 'operand', - 'value', - 'viewId', - ]) - .values( - viewDefinition.filters.map((filter: any) => ({ - fieldMetadataId: filter.fieldMetadataId, - displayValue: filter.displayValue, - operand: filter.operand, - value: filter.value, - viewId: viewDefinition.id, - })), - ) - .execute(); - } - - await this.workspaceMetadataVersionService.incrementMetadataVersion( - workspaceId, - ); - } - } - - // This is an attempt to do something more generic that could be reused in every command - // Next step if it works well for a few command is to isolated it into a file so - // it can be reused and not copy-pasted. - async sharedBoilerplate( - _passedParam: string[], - options: UpdateActivitiesCommandOptions, - coreLogic: CoreLogicFunction, - ) { - const workspaceIds = options.workspaceId - ? [options.workspaceId] - : await this.workspaceStatusService.getActiveWorkspaceIds(); - - if (!workspaceIds.length) { - this.logger.log(chalk.yellow('No workspace found')); - - return; - } - - this.logger.log( - chalk.green(`Running command on ${workspaceIds.length} workspaces`), - ); - - const requiresQueryRunner = - coreLogic.toString().includes('queryRunner') || - coreLogic.toString().includes('schema'); - - for (const workspaceId of workspaceIds) { - try { - if (requiresQueryRunner) { - await this.executeWithQueryRunner(workspaceId, coreLogic); - } else { - await coreLogic({ workspaceId }); - } - - this.logger.log( - chalk.green(`Running command on workspace ${workspaceId} done`), - ); - } catch (error) { - this.logger.error( - `Migration failed for workspace ${workspaceId}: ${error.message}, ${error.stack}`, - ); - } - } - - this.logger.log(chalk.green(`Command completed!`)); - } - - private async executeWithQueryRunner( - workspaceId: string, - coreLogic: CoreLogicFunction, - ) { - const dataSourceMetadatas = - await this.dataSourceService.getDataSourcesMetadataFromWorkspaceId( - workspaceId, - ); - - for (const dataSourceMetadata of dataSourceMetadatas) { - const workspaceDataSource = - await this.typeORMService.connectToDataSource(dataSourceMetadata); - - if (workspaceDataSource) { - const queryRunner = workspaceDataSource.createQueryRunner(); - - await queryRunner.connect(); - await queryRunner.startTransaction(); - - try { - await coreLogic({ - workspaceId, - queryRunner, - schema: dataSourceMetadata.schema, - }); - await queryRunner.commitTransaction(); - } catch (error) { - await queryRunner.rollbackTransaction(); - this.logger.log( - chalk.red(`Running command on workspace ${workspaceId} failed`), - ); - throw error; - } finally { - await queryRunner.release(); - } - } - } - } -} diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-23/0-23-update-file-folder-structure.command.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-23/0-23-update-file-folder-structure.command.ts deleted file mode 100644 index bc549461181c..000000000000 --- a/packages/twenty-server/src/database/commands/upgrade-version/0-23/0-23-update-file-folder-structure.command.ts +++ /dev/null @@ -1,235 +0,0 @@ -import { Logger } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; - -import chalk from 'chalk'; -import { Command, CommandRunner, Option } from 'nest-commander'; -import pLimit from 'p-limit'; -import { Like, Repository } from 'typeorm'; - -import { FileFolder } from 'src/engine/core-modules/file/interfaces/file-folder.interface'; -import { - FileStorageException, - FileStorageExceptionCode, -} from 'src/engine/integrations/file-storage/interfaces/file-storage-exception'; - -import { TypeORMService } from 'src/database/typeorm/typeorm.service'; -import { - Workspace, - WorkspaceActivationStatus, -} from 'src/engine/core-modules/workspace/workspace.entity'; -import { FileStorageService } from 'src/engine/integrations/file-storage/file-storage.service'; -import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; - -interface UpdateFileFolderStructureCommandOptions { - workspaceId?: string; -} - -@Command({ - name: 'upgrade-0-23:update-file-folder-structure', - description: 'Update file folder structure (prefixed per workspace)', -}) -export class UpdateFileFolderStructureCommand extends CommandRunner { - private readonly logger = new Logger(UpdateFileFolderStructureCommand.name); - constructor( - @InjectRepository(Workspace, 'core') - private readonly workspaceRepository: Repository, - private readonly typeORMService: TypeORMService, - private readonly dataSourceService: DataSourceService, - private readonly fileStorageService: FileStorageService, - ) { - super(); - } - - @Option({ - flags: '-w, --workspace-id [workspace_id]', - description: 'workspace id. Command runs on all workspaces if not provided', - required: false, - }) - parseWorkspaceId(value: string): string { - return value; - } - - async run( - _passedParam: string[], - options: UpdateFileFolderStructureCommandOptions, - ): Promise { - const workspaceIds = options.workspaceId - ? [options.workspaceId] - : ( - await this.workspaceRepository.find({ - where: { activationStatus: WorkspaceActivationStatus.ACTIVE }, - }) - ).map((workspace) => workspace.id); - - if (!workspaceIds.length) { - this.logger.log(chalk.yellow('No workspace found')); - - return; - } - - this.logger.log( - chalk.green(`Running command on ${workspaceIds.length} workspaces`), - ); - - for (const workspaceId of workspaceIds) { - const dataSourceMetadata = - await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceId( - workspaceId, - ); - - if (!dataSourceMetadata) { - this.logger.log( - `Could not find dataSourceMetadata for workspace ${workspaceId}`, - ); - continue; - } - - const workspaceDataSource = - await this.typeORMService.connectToDataSource(dataSourceMetadata); - - if (!workspaceDataSource) { - throw new Error( - `Could not connect to dataSource for workspace ${workspaceId}`, - ); - } - - const workspaceQueryRunner = workspaceDataSource.createQueryRunner(); - - const attachmentsToMove = (await workspaceQueryRunner.query( - `SELECT id, "fullPath" FROM "${dataSourceMetadata.schema}"."attachment" WHERE "fullPath" LIKE '${FileFolder.Attachment}/%'`, - )) as { id: string; fullPath: string }[]; - - const workspaceMemberAvatarsToMove = (await workspaceQueryRunner.query( - `SELECT id, "avatarUrl" as "fullPath" FROM "${dataSourceMetadata.schema}"."workspaceMember" WHERE "avatarUrl" LIKE '${FileFolder.ProfilePicture}/%'`, - )) as { id: string; fullPath: string }[]; - - const personAvatarsToMove = (await workspaceQueryRunner.query( - `SELECT id, "avatarUrl" as "fullPath" FROM "${dataSourceMetadata.schema}"."person" WHERE "avatarUrl" LIKE '${FileFolder.PersonPicture}/%'`, - )) as { id: string; fullPath: string }[]; - - const workspacePictureToMove = await this.workspaceRepository.findOneBy({ - id: workspaceId, - logo: Like(`${FileFolder.WorkspaceLogo}/%`), - }); - - try { - const updatedAttachments = await this.moveFiles( - workspaceId, - attachmentsToMove, - ); - - this.logger.log( - chalk.green( - `Moved ${updatedAttachments.length} attachments in workspace ${workspaceId}`, - ), - ); - } catch (e) { - this.logger.error(e); - } - - try { - const updatedWorkspaceMemberAvatars = await this.moveFiles( - workspaceId, - workspaceMemberAvatarsToMove, - ); - - this.logger.log( - chalk.green( - `Moved ${updatedWorkspaceMemberAvatars.length} workspaceMemberAvatars in workspace ${workspaceId}`, - ), - ); - } catch (e) { - this.logger.error(e); - } - - try { - const updatedPersonAvatars = await this.moveFiles( - workspaceId, - personAvatarsToMove, - ); - - this.logger.log( - chalk.green( - `Moved ${updatedPersonAvatars.length} personAvatars in workspace ${workspaceId}`, - ), - ); - } catch (e) { - this.logger.error(e); - } - - if (workspacePictureToMove?.logo) { - await this.moveFiles(workspaceId, [ - { - id: workspacePictureToMove.id, - fullPath: workspacePictureToMove.logo, - }, - ]); - - this.logger.log( - chalk.green(`Moved workspacePicture in workspace ${workspaceId}`), - ); - } - - this.logger.log( - chalk.green(`Running command on workspace ${workspaceId} done`), - ); - } - - this.logger.log(chalk.green(`Command completed!`)); - } - - private async moveFiles( - workspaceId: string, - filesToMove: { id: string; fullPath: string }[], - ): Promise> { - const batchSize = 20; - const limit = pLimit(batchSize); - - const moveFile = async ({ - id, - fullPath, - }: { - id: string; - fullPath: string; - }) => { - const pathParts = fullPath.split('/'); - const filename = pathParts.pop(); - - if (!filename) { - throw new Error(`Filename is empty for file ID: ${id}`); - } - - const originalFolderPath = pathParts.join('/'); - const updatedFolderPath = `workspace-${workspaceId}/${originalFolderPath}`; - - try { - await this.fileStorageService.move({ - from: { folderPath: originalFolderPath, filename }, - to: { folderPath: updatedFolderPath, filename }, - }); - } catch (error) { - if ( - error instanceof FileStorageException && - error.code === FileStorageExceptionCode.FILE_NOT_FOUND - ) { - this.logger.error(`File not found: ${fullPath}`); - } else { - this.logger.error(`Error moving file ${fullPath}: ${error}`); - } - - return; - } - - return { id, updatedFolderPath }; - }; - - const movePromises = filesToMove.map((file) => limit(() => moveFile(file))); - - const results = await Promise.all(movePromises); - - return results.filter( - (result): result is { id: string; updatedFolderPath: string } => - Boolean(result), - ); - } -} diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-23/0-23-upgrade-version.command.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-23/0-23-upgrade-version.command.ts deleted file mode 100644 index 77ef0ef79b37..000000000000 --- a/packages/twenty-server/src/database/commands/upgrade-version/0-23/0-23-upgrade-version.command.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { Command, CommandRunner, Option } from 'nest-commander'; - -import { BackfillNewOnboardingUserVarsCommand } from 'src/database/commands/upgrade-version/0-23/0-23-backfill-new-onboarding-user-vars'; -import { MigrateDomainNameFromTextToLinksCommand } from 'src/database/commands/upgrade-version/0-23/0-23-migrate-domain-to-links.command'; -import { MigrateLinkFieldsToLinksCommand } from 'src/database/commands/upgrade-version/0-23/0-23-migrate-link-fields-to-links.command'; -import { MigrateMessageChannelSyncStatusEnumCommand } from 'src/database/commands/upgrade-version/0-23/0-23-migrate-message-channel-sync-status-enum.command'; -import { SetUserVarsAccountsToReconnectCommand } from 'src/database/commands/upgrade-version/0-23/0-23-set-user-vars-accounts-to-reconnect.command'; -import { SetWorkspaceActivationStatusCommand } from 'src/database/commands/upgrade-version/0-23/0-23-set-workspace-activation-status.command'; -import { UpdateActivitiesCommand } from 'src/database/commands/upgrade-version/0-23/0-23-update-activities.command'; -import { UpdateFileFolderStructureCommand } from 'src/database/commands/upgrade-version/0-23/0-23-update-file-folder-structure.command'; -import { SyncWorkspaceMetadataCommand } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/sync-workspace-metadata.command'; - -interface UpdateTo0_23CommandOptions { - workspaceId?: string; -} - -@Command({ - name: 'upgrade-0.23', - description: 'Upgrade to 0.23', -}) -export class UpgradeTo0_23Command extends CommandRunner { - constructor( - private readonly syncWorkspaceMetadataCommand: SyncWorkspaceMetadataCommand, - private readonly updateFileFolderStructureCommandOptions: UpdateFileFolderStructureCommand, - private readonly migrateLinkFieldsToLinks: MigrateLinkFieldsToLinksCommand, - private readonly migrateDomainNameFromTextToLinks: MigrateDomainNameFromTextToLinksCommand, - private readonly migrateMessageChannelSyncStatusEnumCommand: MigrateMessageChannelSyncStatusEnumCommand, - private readonly setWorkspaceActivationStatusCommand: SetWorkspaceActivationStatusCommand, - private readonly updateActivitiesCommand: UpdateActivitiesCommand, - private readonly backfillNewOnboardingUserVarsCommand: BackfillNewOnboardingUserVarsCommand, - private readonly setUserVarsAccountsToReconnectCommand: SetUserVarsAccountsToReconnectCommand, - ) { - super(); - } - - @Option({ - flags: '-w, --workspace-id [workspace_id]', - description: - 'workspace id. Command runs on all active workspaces if not provided', - required: false, - }) - parseWorkspaceId(value: string): string { - return value; - } - - async run( - _passedParam: string[], - options: UpdateTo0_23CommandOptions, - ): Promise { - await this.migrateLinkFieldsToLinks.run(_passedParam, options); - await this.migrateDomainNameFromTextToLinks.run(_passedParam, options); - await this.migrateMessageChannelSyncStatusEnumCommand.run( - _passedParam, - options, - ); - await this.setWorkspaceActivationStatusCommand.run(_passedParam, options); - await this.updateFileFolderStructureCommandOptions.run( - _passedParam, - options, - ); - await this.syncWorkspaceMetadataCommand.run(_passedParam, { - ...options, - force: true, - }); - await this.updateActivitiesCommand.run(_passedParam, options); - await this.backfillNewOnboardingUserVarsCommand.run(_passedParam, options); - await this.setUserVarsAccountsToReconnectCommand.run(_passedParam, options); - } -} diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-23/0-23-upgrade-version.module.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-23/0-23-upgrade-version.module.ts deleted file mode 100644 index 09fbe2fb9f98..000000000000 --- a/packages/twenty-server/src/database/commands/upgrade-version/0-23/0-23-upgrade-version.module.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { Module } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; - -import { BackfillNewOnboardingUserVarsCommand } from 'src/database/commands/upgrade-version/0-23/0-23-backfill-new-onboarding-user-vars'; -import { MigrateDomainNameFromTextToLinksCommand } from 'src/database/commands/upgrade-version/0-23/0-23-migrate-domain-to-links.command'; -import { MigrateLinkFieldsToLinksCommand } from 'src/database/commands/upgrade-version/0-23/0-23-migrate-link-fields-to-links.command'; -import { MigrateMessageChannelSyncStatusEnumCommand } from 'src/database/commands/upgrade-version/0-23/0-23-migrate-message-channel-sync-status-enum.command'; -import { SetUserVarsAccountsToReconnectCommand } from 'src/database/commands/upgrade-version/0-23/0-23-set-user-vars-accounts-to-reconnect.command'; -import { SetWorkspaceActivationStatusCommand } from 'src/database/commands/upgrade-version/0-23/0-23-set-workspace-activation-status.command'; -import { UpdateActivitiesCommand } from 'src/database/commands/upgrade-version/0-23/0-23-update-activities.command'; -import { UpdateFileFolderStructureCommand } from 'src/database/commands/upgrade-version/0-23/0-23-update-file-folder-structure.command'; -import { UpgradeTo0_23Command } from 'src/database/commands/upgrade-version/0-23/0-23-upgrade-version.command'; -import { TypeORMModule } from 'src/database/typeorm/typeorm.module'; -import { BillingModule } from 'src/engine/core-modules/billing/billing.module'; -import { KeyValuePair } from 'src/engine/core-modules/key-value-pair/key-value-pair.entity'; -import { OnboardingModule } from 'src/engine/core-modules/onboarding/onboarding.module'; -import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; -import { FileStorageModule } from 'src/engine/integrations/file-storage/file-storage.module'; -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 { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module'; -import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module'; -import { WorkspaceStatusModule } from 'src/engine/workspace-manager/workspace-status/workspace-manager.module'; -import { WorkspaceSyncMetadataCommandsModule } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/workspace-sync-metadata-commands.module'; -import { ConnectedAccountModule } from 'src/modules/connected-account/connected-account.module'; -import { ViewModule } from 'src/modules/view/view.module'; - -@Module({ - imports: [ - TypeOrmModule.forFeature([Workspace, KeyValuePair], 'core'), - WorkspaceSyncMetadataCommandsModule, - FileStorageModule, - OnboardingModule, - TypeORMModule, - DataSourceModule, - WorkspaceMetadataVersionModule, - FieldMetadataModule, - DataSourceModule, - WorkspaceStatusModule, - TypeOrmModule.forFeature([FieldMetadataEntity], 'metadata'), - TypeOrmModule.forFeature([ObjectMetadataEntity], 'metadata'), - TypeORMModule, - ViewModule, - BillingModule, - ObjectMetadataModule, - ConnectedAccountModule, - ], - providers: [ - UpdateFileFolderStructureCommand, - MigrateLinkFieldsToLinksCommand, - MigrateDomainNameFromTextToLinksCommand, - MigrateMessageChannelSyncStatusEnumCommand, - SetWorkspaceActivationStatusCommand, - UpdateActivitiesCommand, - BackfillNewOnboardingUserVarsCommand, - SetUserVarsAccountsToReconnectCommand, - UpgradeTo0_23Command, - ], -}) -export class UpgradeTo0_23CommandModule {} diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-24/0-24-set-message-direction.command.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-24/0-24-set-message-direction.command.ts new file mode 100644 index 000000000000..b8689ecfaad5 --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version/0-24/0-24-set-message-direction.command.ts @@ -0,0 +1,222 @@ +import { Logger } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; + +import chalk from 'chalk'; +import { Command, CommandRunner, Option } from 'nest-commander'; +import { Any, Repository } from 'typeorm'; + +import { + Workspace, + WorkspaceActivationStatus, +} from 'src/engine/core-modules/workspace/workspace.entity'; +import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.service'; +import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { MessageDirection } from 'src/modules/messaging/common/enums/message-direction.enum'; +import { MessageChannelMessageAssociationWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel-message-association.workspace-entity'; + +interface SetMessageDirectionCommandOptions { + workspaceId?: string; +} + +const MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_BATCH_SIZE = 10; + +@Command({ + name: 'upgrade-0.24:set-message-direction', + description: 'Set message direction', +}) +export class SetMessageDirectionCommand extends CommandRunner { + private readonly logger = new Logger(SetMessageDirectionCommand.name); + constructor( + private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService, + private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + @InjectRepository(Workspace, 'core') + private readonly workspaceRepository: Repository, + ) { + super(); + } + + @Option({ + flags: '-w, --workspace-id [workspace_id]', + description: 'workspace id. Command runs on all workspaces if not provided', + required: false, + }) + parseWorkspaceId(value: string): string { + return value; + } + + async run( + _passedParam: string[], + options: SetMessageDirectionCommandOptions, + ): Promise { + let activeWorkspaceIds: string[] = []; + + if (options.workspaceId) { + activeWorkspaceIds = [options.workspaceId]; + } else { + const activeWorkspaces = await this.workspaceRepository.find({ + where: { + activationStatus: WorkspaceActivationStatus.ACTIVE, + ...(options.workspaceId && { id: options.workspaceId }), + }, + }); + + activeWorkspaceIds = activeWorkspaces.map((workspace) => workspace.id); + } + + if (!activeWorkspaceIds.length) { + this.logger.log(chalk.yellow('No workspace found')); + + return; + } else { + this.logger.log( + chalk.green( + `Running command on ${activeWorkspaceIds.length} workspaces`, + ), + ); + } + + for (const workspaceId of activeWorkspaceIds) { + try { + const messageChannelMessageAssociationRepository = + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + workspaceId, + 'messageChannelMessageAssociation', + ); + + const workspaceDataSource = + await this.twentyORMGlobalManager.getDataSourceForWorkspace( + workspaceId, + ); + + await workspaceDataSource.transaction(async (transactionManager) => { + try { + const messageChannelMessageAssociationCount = + await messageChannelMessageAssociationRepository.count( + {}, + transactionManager, + ); + + for ( + let i = 0; + i < messageChannelMessageAssociationCount; + i += MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_BATCH_SIZE + ) { + const messageChannelMessageAssociationsPage = + await messageChannelMessageAssociationRepository.find( + { + where: { + message: { + messageParticipants: { + role: 'from', + }, + }, + }, + relations: { + message: { + messageParticipants: true, + }, + messageChannel: { + connectedAccount: true, + }, + }, + take: MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_BATCH_SIZE, + skip: i, + }, + transactionManager, + ); + + const { incoming, outgoing } = + messageChannelMessageAssociationsPage.reduce( + ( + acc: { + incoming: string[]; + outgoing: string[]; + }, + messageChannelMessageAssociation, + ) => { + const connectedAccountHandle = + messageChannelMessageAssociation?.messageChannel + ?.connectedAccount?.handle; + const connectedAccountHandleAliases = + messageChannelMessageAssociation?.messageChannel + ?.connectedAccount?.handleAliases; + const fromHandle = + messageChannelMessageAssociation?.message + ?.messageParticipants?.[0]?.handle ?? ''; + + if ( + connectedAccountHandle === fromHandle || + connectedAccountHandleAliases?.includes(fromHandle) + ) { + acc.outgoing.push(messageChannelMessageAssociation.id); + } else { + acc.incoming.push(messageChannelMessageAssociation.id); + } + + return acc; + }, + { incoming: [], outgoing: [] }, + ); + + await messageChannelMessageAssociationRepository.update( + { + id: Any(incoming), + }, + { + direction: MessageDirection.INCOMING, + }, + transactionManager, + ); + + await messageChannelMessageAssociationRepository.update( + { + id: Any(outgoing), + }, + { + direction: MessageDirection.OUTGOING, + }, + transactionManager, + ); + + const numberOfProcessedAssociations = + i + MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_BATCH_SIZE; + + if ( + numberOfProcessedAssociations % + (MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_BATCH_SIZE * 10) === + 0 || + numberOfProcessedAssociations >= + messageChannelMessageAssociationCount + ) { + this.logger.log( + chalk.green( + `Processed ${Math.min(numberOfProcessedAssociations, messageChannelMessageAssociationCount)} of ${messageChannelMessageAssociationCount} message channel message associations`, + ), + ); + } + } + } catch (error) { + this.logger.log( + chalk.red(`Running command on workspace ${workspaceId} failed`), + ); + throw error; + } + }); + + await this.workspaceMetadataVersionService.incrementMetadataVersion( + workspaceId, + ); + + this.logger.log( + chalk.green(`Running command on workspace ${workspaceId} done`), + ); + } catch (error) { + this.logger.error( + `Migration failed for workspace ${workspaceId}: ${error.message}`, + ); + } + } + + this.logger.log(chalk.green(`Command completed!`)); + } +} diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-24/0-24-upgrade-version.command.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-24/0-24-upgrade-version.command.ts new file mode 100644 index 000000000000..08b32b05e235 --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version/0-24/0-24-upgrade-version.command.ts @@ -0,0 +1,42 @@ +import { Command, CommandRunner, Option } from 'nest-commander'; + +import { SetMessageDirectionCommand } from 'src/database/commands/upgrade-version/0-24/0-24-set-message-direction.command'; +import { SyncWorkspaceMetadataCommand } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/sync-workspace-metadata.command'; + +interface UpdateTo0_24CommandOptions { + workspaceId?: string; +} + +@Command({ + name: 'upgrade-0.24', + description: 'Upgrade to 0.24', +}) +export class UpgradeTo0_24Command extends CommandRunner { + constructor( + private readonly syncWorkspaceMetadataCommand: SyncWorkspaceMetadataCommand, + private readonly setMessagesDirectionCommand: SetMessageDirectionCommand, + ) { + super(); + } + + @Option({ + flags: '-w, --workspace-id [workspace_id]', + description: + 'workspace id. Command runs on all active workspaces if not provided', + required: false, + }) + parseWorkspaceId(value: string): string { + return value; + } + + async run( + _passedParam: string[], + options: UpdateTo0_24CommandOptions, + ): Promise { + await this.syncWorkspaceMetadataCommand.run(_passedParam, { + ...options, + force: true, + }); + await this.setMessagesDirectionCommand.run(_passedParam, options); + } +} diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-24/0-24-upgrade-version.module.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-24/0-24-upgrade-version.module.ts new file mode 100644 index 000000000000..b718b9ed249c --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version/0-24/0-24-upgrade-version.module.ts @@ -0,0 +1,38 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +import { SetMessageDirectionCommand } from 'src/database/commands/upgrade-version/0-24/0-24-set-message-direction.command'; +import { UpgradeTo0_24Command } from 'src/database/commands/upgrade-version/0-24/0-24-upgrade-version.command'; +import { TypeORMModule } from 'src/database/typeorm/typeorm.module'; +import { KeyValuePair } from 'src/engine/core-modules/key-value-pair/key-value-pair.entity'; +import { OnboardingModule } from 'src/engine/core-modules/onboarding/onboarding.module'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { FileStorageModule } from 'src/engine/integrations/file-storage/file-storage.module'; +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 { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module'; +import { WorkspaceStatusModule } from 'src/engine/workspace-manager/workspace-status/workspace-manager.module'; +import { WorkspaceSyncMetadataCommandsModule } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/workspace-sync-metadata-commands.module'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([Workspace, KeyValuePair], 'core'), + WorkspaceSyncMetadataCommandsModule, + FileStorageModule, + OnboardingModule, + TypeORMModule, + DataSourceModule, + WorkspaceMetadataVersionModule, + FieldMetadataModule, + WorkspaceStatusModule, + TypeOrmModule.forFeature( + [FieldMetadataEntity, ObjectMetadataEntity], + 'metadata', + ), + TypeORMModule, + ], + providers: [UpgradeTo0_24Command, SetMessageDirectionCommand], +}) +export class UpgradeTo0_24CommandModule {} diff --git a/packages/twenty-server/src/database/commands/upgrade-version/upgrade-version.command.ts b/packages/twenty-server/src/database/commands/upgrade-version/upgrade-version.command.ts deleted file mode 100644 index 0fc4fa9f89bc..000000000000 --- a/packages/twenty-server/src/database/commands/upgrade-version/upgrade-version.command.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { Logger } from '@nestjs/common'; -import { isUndefined } from '@nestjs/common/utils/shared.utils'; - -import * as fs from 'fs'; -import * as path from 'path'; - -import chalk from 'chalk'; -import { Command, CommandRunner, Option } from 'nest-commander'; -import * as semver from 'semver'; - -import { MigrateDomainNameFromTextToLinksCommand } from 'src/database/commands/upgrade-version/0-23/0-23-migrate-domain-to-links.command'; -import { MigrateLinkFieldsToLinksCommand } from 'src/database/commands/upgrade-version/0-23/0-23-migrate-link-fields-to-links.command'; -import { MigrateMessageChannelSyncStatusEnumCommand } from 'src/database/commands/upgrade-version/0-23/0-23-migrate-message-channel-sync-status-enum.command'; -import { SetWorkspaceActivationStatusCommand } from 'src/database/commands/upgrade-version/0-23/0-23-set-workspace-activation-status.command'; -import { UpdateActivitiesCommand } from 'src/database/commands/upgrade-version/0-23/0-23-update-activities.command'; - -interface UpgradeCommandOptions { - workspaceId?: string; -} - -type VersionUpgradeMap = { - [version: string]: CommandRunner[]; -}; - -@Command({ - name: 'upgrade-version', - description: 'Upgrade to a specific version', -}) -export class UpgradeVersionCommand extends CommandRunner { - private readonly logger = new Logger(UpgradeVersionCommand.name); - - constructor( - private readonly migrateLinkFieldsToLinksCommand: MigrateLinkFieldsToLinksCommand, - private readonly migrateDomainNameFromTextToLinksCommand: MigrateDomainNameFromTextToLinksCommand, - private readonly migrateMessageChannelSyncStatusEnumCommand: MigrateMessageChannelSyncStatusEnumCommand, - private readonly setWorkspaceActivationStatusCommand: SetWorkspaceActivationStatusCommand, - private readonly updateActivitiesCommand: UpdateActivitiesCommand, - ) { - super(); - } - - @Option({ - flags: '-v, --version ', - description: 'Version to upgrade to', - required: true, - }) - parseVersion(value: string): string { - return value; - } - - @Option({ - flags: '-w, --workspace-id [workspace_id]', - description: 'workspace id. Command runs on all workspaces if not provided', - required: false, - }) - parseWorkspaceId(value: string): string { - return value; - } - - async run( - passedParams: string[], - options: UpgradeCommandOptions & { version: string }, - ): Promise { - const { version, ...upgradeOptions } = options; - - const versionUpgradeMap = { - '0.23': [ - this.migrateLinkFieldsToLinksCommand, - this.migrateDomainNameFromTextToLinksCommand, - this.migrateMessageChannelSyncStatusEnumCommand, - this.setWorkspaceActivationStatusCommand, - this.updateActivitiesCommand, - ], - }; - - await this.validateVersions(version, versionUpgradeMap); - - if (!versionUpgradeMap[version]) { - throw new Error( - `No migration commands found for version ${version}. This could mean there were no database changes required for this version.`, - ); - } - - for (const command of versionUpgradeMap[version]) { - await command.run(passedParams, upgradeOptions); - } - - this.logger.log(chalk.green(`Successfully upgraded to version ${version}`)); - } - - private async getCurrentCodeVersion(): Promise { - const packageJsonPath = path.join(process.cwd(), 'package.json'); - const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); - - return packageJson.version; - } - - private async validateVersions( - targetVersion: string, - versionUpgradeMap: VersionUpgradeMap, - ): Promise { - const currentVersion = await this.getCurrentCodeVersion(); - - const cleanCurrentVersion = semver.coerce(currentVersion); - const cleanTargetVersion = semver.coerce(targetVersion); - - if (!cleanCurrentVersion || !cleanTargetVersion) { - throw new Error( - `Invalid version format. Current Code: ${currentVersion}, Target: ${targetVersion}`, - ); - } - - const targetMajorMinor = `${cleanTargetVersion.major}.${cleanTargetVersion.minor}`; - - if ( - semver.gt(cleanTargetVersion, cleanCurrentVersion) && - isUndefined(versionUpgradeMap[targetMajorMinor]) - ) { - throw new Error( - `Cannot upgrade to ${cleanTargetVersion}. Your current code version is ${cleanCurrentVersion}. Please update your codebase or upgrade your Docker image first.`, - ); - } - - this.logger.log( - `Current Code Version: ${currentVersion}, Target: ${targetVersion}`, - ); - } -} diff --git a/packages/twenty-server/src/database/commands/upgrade-version/upgrade-version.module.ts b/packages/twenty-server/src/database/commands/upgrade-version/upgrade-version.module.ts deleted file mode 100644 index 2cfefd9d0b0a..000000000000 --- a/packages/twenty-server/src/database/commands/upgrade-version/upgrade-version.module.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { Module } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; - -import { DataSeedDemoWorkspaceModule } from 'src/database/commands/data-seed-demo-workspace/data-seed-demo-workspace.module'; -import { BackfillNewOnboardingUserVarsCommand } from 'src/database/commands/upgrade-version/0-23/0-23-backfill-new-onboarding-user-vars'; -import { MigrateDomainNameFromTextToLinksCommand } from 'src/database/commands/upgrade-version/0-23/0-23-migrate-domain-to-links.command'; -import { MigrateLinkFieldsToLinksCommand } from 'src/database/commands/upgrade-version/0-23/0-23-migrate-link-fields-to-links.command'; -import { MigrateMessageChannelSyncStatusEnumCommand } from 'src/database/commands/upgrade-version/0-23/0-23-migrate-message-channel-sync-status-enum.command'; -import { SetWorkspaceActivationStatusCommand } from 'src/database/commands/upgrade-version/0-23/0-23-set-workspace-activation-status.command'; -import { UpdateActivitiesCommand } from 'src/database/commands/upgrade-version/0-23/0-23-update-activities.command'; -import { UpgradeVersionCommand } from 'src/database/commands/upgrade-version/upgrade-version.command'; -import { TypeORMModule } from 'src/database/typeorm/typeorm.module'; -import { BillingModule } from 'src/engine/core-modules/billing/billing.module'; -import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity'; -import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; -import { OnboardingModule } from 'src/engine/core-modules/onboarding/onboarding.module'; -import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; -import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module'; -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 { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module'; -import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module'; -import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module'; -import { WorkspaceManagerModule } from 'src/engine/workspace-manager/workspace-manager.module'; -import { WorkspaceStatusModule } from 'src/engine/workspace-manager/workspace-status/workspace-manager.module'; -import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.module'; -import { ViewModule } from 'src/modules/view/view.module'; - -@Module({ - imports: [ - WorkspaceManagerModule, - DataSourceModule, - OnboardingModule, - TypeORMModule, - TypeOrmModule.forFeature( - [Workspace, BillingSubscription, FeatureFlagEntity], - 'core', - ), - TypeOrmModule.forFeature( - [FieldMetadataEntity, ObjectMetadataEntity], - 'metadata', - ), - WorkspaceModule, - WorkspaceDataSourceModule, - WorkspaceSyncMetadataModule, - WorkspaceStatusModule, - ObjectMetadataModule, - DataSeedDemoWorkspaceModule, - WorkspaceMetadataVersionModule, - FieldMetadataModule, - ViewModule, - BillingModule, - ], - providers: [ - UpgradeVersionCommand, - MigrateLinkFieldsToLinksCommand, - MigrateDomainNameFromTextToLinksCommand, - MigrateMessageChannelSyncStatusEnumCommand, - SetWorkspaceActivationStatusCommand, - UpdateActivitiesCommand, - BackfillNewOnboardingUserVarsCommand, - ], -}) -export class UpgradeVersionModule {} diff --git a/packages/twenty-server/src/database/typeorm-seeds/workspace/messages.ts b/packages/twenty-server/src/database/typeorm-seeds/workspace/messages.ts index 231eb23c85f6..f549d43ae485 100644 --- a/packages/twenty-server/src/database/typeorm-seeds/workspace/messages.ts +++ b/packages/twenty-server/src/database/typeorm-seeds/workspace/messages.ts @@ -1,6 +1,7 @@ import { EntityManager } from 'typeorm'; import { DEV_SEED_MESSAGE_THREAD_IDS } from 'src/database/typeorm-seeds/workspace/message-threads'; +import { MessageDirection } from 'src/modules/messaging/common/enums/message-direction.enum'; const tableName = 'message'; @@ -39,7 +40,7 @@ export const seedMessage = async ( receivedAt: new Date(), text: 'Hello, \n I hope this email finds you well. I am writing to request a meeting. I believe it would be beneficial for both parties to collaborate and explore potential opportunities. Would you be available for a meeting sometime next week? Please let me know your availability, and I will arrange a suitable time. \n Looking forward to your response.\n Best regards', subject: 'Meeting Request', - direction: 'outgoing', + direction: MessageDirection.OUTGOING, messageThreadId: DEV_SEED_MESSAGE_THREAD_IDS.MESSAGE_THREAD_1, headerMessageId: '99ef24a8-2b8a-405d-8f42-e820ca921421', }, @@ -51,7 +52,7 @@ export const seedMessage = async ( receivedAt: new Date(), text: 'Good Morning,\n I am writing to inquire about information. Could you please provide me with details regarding this topic? \n Your assistance in this matter would be greatly appreciated. Thank you in advance for your prompt response. \n Best regards,Tim', subject: 'Inquiry Regarding Topic', - direction: 'outgoing', + direction: MessageDirection.OUTGOING, messageThreadId: DEV_SEED_MESSAGE_THREAD_IDS.MESSAGE_THREAD_2, headerMessageId: '8f804a9a-04c8-4f24-93f2-764948e95014', }, @@ -63,7 +64,7 @@ export const seedMessage = async ( receivedAt: new Date(), text: 'Good Evening,\nI wanted to extend my sincere gratitude for taking the time to meet with me earlier today. It was a pleasure discussing with you, and I am excited about the potential opportunities for collaboration. \n Please feel free to reach out if you have any further questions or require additional information. I look forward to our continued communication. Best regards.', subject: 'Thank You for the Meeting', - direction: 'incoming', + direction: MessageDirection.INCOMING, messageThreadId: DEV_SEED_MESSAGE_THREAD_IDS.MESSAGE_THREAD_1, headerMessageId: '3939d68a-ac6b-4f86-87a2-5f5f9d1b6481', }, diff --git a/packages/twenty-server/src/modules/messaging/common/enums/message-direction.enum.ts b/packages/twenty-server/src/modules/messaging/common/enums/message-direction.enum.ts new file mode 100644 index 000000000000..21db64a7345d --- /dev/null +++ b/packages/twenty-server/src/modules/messaging/common/enums/message-direction.enum.ts @@ -0,0 +1,4 @@ +export enum MessageDirection { + INCOMING = 'INCOMING', + OUTGOING = 'OUTGOING', +} diff --git a/packages/twenty-server/src/modules/messaging/common/standard-objects/message-channel-message-association.workspace-entity.ts b/packages/twenty-server/src/modules/messaging/common/standard-objects/message-channel-message-association.workspace-entity.ts index 6c7baf5e80eb..8dc83992788d 100644 --- a/packages/twenty-server/src/modules/messaging/common/standard-objects/message-channel-message-association.workspace-entity.ts +++ b/packages/twenty-server/src/modules/messaging/common/standard-objects/message-channel-message-association.workspace-entity.ts @@ -10,8 +10,12 @@ import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace- import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator'; import { WorkspaceJoinColumn } from 'src/engine/twenty-orm/decorators/workspace-join-column.decorator'; import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator'; -import { MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids'; +import { + MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_STANDARD_FIELD_IDS, + MESSAGE_STANDARD_FIELD_IDS, +} from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids'; import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids'; +import { MessageDirection } from 'src/modules/messaging/common/enums/message-direction.enum'; import { MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; import { MessageWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message.workspace-entity'; @@ -48,6 +52,30 @@ export class MessageChannelMessageAssociationWorkspaceEntity extends BaseWorkspa @WorkspaceIsNullable() messageThreadExternalId: string | null; + @WorkspaceField({ + standardId: MESSAGE_STANDARD_FIELD_IDS.direction, + type: FieldMetadataType.SELECT, + label: 'Direction', + description: 'Message Direction', + icon: 'IconDirection', + options: [ + { + value: MessageDirection.INCOMING, + label: 'Incoming', + position: 0, + color: 'green', + }, + { + value: MessageDirection.OUTGOING, + label: 'Outgoing', + position: 1, + color: 'blue', + }, + ], + defaultValue: MessageDirection.INCOMING, + }) + direction: MessageDirection; + @WorkspaceRelation({ standardId: MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_STANDARD_FIELD_IDS.messageChannel, diff --git a/packages/twenty-server/src/modules/messaging/common/standard-objects/message.workspace-entity.ts b/packages/twenty-server/src/modules/messaging/common/standard-objects/message.workspace-entity.ts index 535639adce25..82702a773f8e 100644 --- a/packages/twenty-server/src/modules/messaging/common/standard-objects/message.workspace-entity.ts +++ b/packages/twenty-server/src/modules/messaging/common/standard-objects/message.workspace-entity.ts @@ -40,20 +40,6 @@ export class MessageWorkspaceEntity extends BaseWorkspaceEntity { }) headerMessageId: string; - @WorkspaceField({ - standardId: MESSAGE_STANDARD_FIELD_IDS.direction, - type: FieldMetadataType.SELECT, - label: 'Direction', - description: 'Message Direction', - icon: 'IconDirection', - options: [ - { value: 'incoming', label: 'Incoming', position: 0, color: 'green' }, - { value: 'outgoing', label: 'Outgoing', position: 1, color: 'blue' }, - ], - defaultValue: "'incoming'", - }) - direction: string; - @WorkspaceField({ standardId: MESSAGE_STANDARD_FIELD_IDS.subject, type: FieldMetadataType.TEXT, diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/utils/compute-message-direction.util.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/utils/compute-message-direction.util.ts index 77cf13fbffa5..4b0c72c97d08 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/utils/compute-message-direction.util.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/utils/compute-message-direction.util.ts @@ -1,4 +1,5 @@ import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; +import { MessageDirection } from 'src/modules/messaging/common/enums/message-direction.enum'; export const computeMessageDirection = ( fromHandle: string, @@ -6,8 +7,8 @@ export const computeMessageDirection = ( ConnectedAccountWorkspaceEntity, 'handle' | 'handleAliases' >, -): 'outgoing' | 'incoming' => +): MessageDirection => connectedAccount.handle === fromHandle || connectedAccount.handleAliases?.includes(fromHandle) - ? 'outgoing' - : 'incoming'; + ? MessageDirection.OUTGOING + : MessageDirection.INCOMING; diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-message.service.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-message.service.ts index a69b92448ee3..f8e5e02c2012 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-message.service.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-message.service.ts @@ -104,7 +104,6 @@ export class MessagingMessageService { headerMessageId: message.headerMessageId, subject: message.subject, receivedAt: message.receivedAt, - direction: message.direction, text: message.text, messageThreadId: newOrExistingMessageThreadId, }, @@ -119,6 +118,7 @@ export class MessagingMessageService { messageId: newMessageId, messageExternalId: message.externalId, messageThreadExternalId: message.messageThreadExternalId, + direction: message.direction, }, transactionManager, ); diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/types/message.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/types/message.ts index a58f67b47daf..4ec483869a8d 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/types/message.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/types/message.ts @@ -1,3 +1,4 @@ +import { MessageDirection } from 'src/modules/messaging/common/enums/message-direction.enum'; import { MessageParticipantWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-participant.workspace-entity'; import { MessageWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message.workspace-entity'; @@ -16,6 +17,7 @@ export type Message = Omit< }[]; externalId: string; messageThreadExternalId: string; + direction: MessageDirection; }; export type MessageParticipant = Omit< diff --git a/packages/twenty-server/src/modules/messaging/message-participant-manager/jobs/messaging-create-company-and-contact-after-sync.job.ts b/packages/twenty-server/src/modules/messaging/message-participant-manager/jobs/messaging-create-company-and-contact-after-sync.job.ts index 36be02e7349f..e6b7c59f21c4 100644 --- a/packages/twenty-server/src/modules/messaging/message-participant-manager/jobs/messaging-create-company-and-contact-after-sync.job.ts +++ b/packages/twenty-server/src/modules/messaging/message-participant-manager/jobs/messaging-create-company-and-contact-after-sync.job.ts @@ -11,6 +11,7 @@ import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; import { ConnectedAccountRepository } from 'src/modules/connected-account/repositories/connected-account.repository'; import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; import { CreateCompanyAndContactService } from 'src/modules/contact-creation-manager/services/create-company-and-contact.service'; +import { MessageDirection } from 'src/modules/messaging/common/enums/message-direction.enum'; import { MessageChannelContactAutoCreationPolicy, MessageChannelWorkspaceEntity, @@ -81,16 +82,16 @@ export class MessagingCreateCompanyAndContactAfterSyncJob { const directionFilter = contactAutoCreationPolicy === MessageChannelContactAutoCreationPolicy.SENT_AND_RECEIVED - ? Any(['incoming', 'outgoing']) - : 'outgoing'; + ? Any([MessageDirection.INCOMING, MessageDirection.OUTGOING]) + : MessageDirection.OUTGOING; const contactsToCreate = await messageParticipantRepository.find({ where: { message: { messageChannelMessageAssociations: { messageChannelId, + direction: directionFilter, }, - direction: directionFilter, }, personId: IsNull(), workspaceMemberId: IsNull(),