From 8e25a107fd4324f6c470f9fa3159b9a75700ba02 Mon Sep 17 00:00:00 2001 From: Marie <51697796+ijreilly@users.noreply.github.com> Date: Thu, 11 Jul 2024 14:39:38 +0200 Subject: [PATCH] Add new Address field to views containing deprecated address (#6205) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit as per title, following introduction of new Address field, we want to display the new field next to the deprecated field, for users to notice the new field. Capture d’écran 2024-07-10 à 17 44 25 --- ...o-views-with-deprecated-address.command.ts | 230 ++++++++++++++++++ .../commands/database-command.module.ts | 9 +- .../listeners/entity-events-to-db.listener.ts | 2 +- .../company.workspace-entity.ts | 20 +- 4 files changed, 249 insertions(+), 12 deletions(-) create mode 100644 packages/twenty-server/src/database/commands/0-22-add-new-address-field-to-views-with-deprecated-address.command.ts diff --git a/packages/twenty-server/src/database/commands/0-22-add-new-address-field-to-views-with-deprecated-address.command.ts b/packages/twenty-server/src/database/commands/0-22-add-new-address-field-to-views-with-deprecated-address.command.ts new file mode 100644 index 000000000..a21b43975 --- /dev/null +++ b/packages/twenty-server/src/database/commands/0-22-add-new-address-field-to-views-with-deprecated-address.command.ts @@ -0,0 +1,230 @@ +import { Logger } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; + +import chalk from 'chalk'; +import isEmpty from 'lodash.isempty'; +import { Command, CommandRunner, Option } from 'nest-commander'; +import { Repository } from 'typeorm'; + +import { TypeORMService } from 'src/database/typeorm/typeorm.service'; +import { + BillingSubscription, + SubscriptionStatus, +} from 'src/engine/core-modules/billing/entities/billing-subscription.entity'; +import { + FeatureFlagEntity, + FeatureFlagKeys, +} from 'src/engine/core-modules/feature-flag/feature-flag.entity'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +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 { WorkspaceCacheVersionService } from 'src/engine/metadata-modules/workspace-cache-version/workspace-cache-version.service'; +import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; +import { COMPANY_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids'; +import { ViewFieldWorkspaceEntity } from 'src/modules/view/standard-objects/view-field.workspace-entity'; + +interface AddNewAddressFieldToViewsWithDeprecatedAddressFieldCommandOptions { + workspaceId?: string; +} + +@Command({ + name: 'migrate-0.22:add-new-address-field-to-views-with-deprecated-address-field', + description: 'Adding new field Address to views containing old address field', +}) +export class AddNewAddressFieldToViewsWithDeprecatedAddressFieldCommand extends CommandRunner { + private readonly logger = new Logger( + AddNewAddressFieldToViewsWithDeprecatedAddressFieldCommand.name, + ); + constructor( + @InjectRepository(Workspace, 'core') + private readonly workspaceRepository: Repository, + @InjectRepository(BillingSubscription, 'core') + private readonly billingSubscriptionRepository: Repository, + @InjectRepository(FeatureFlagEntity, 'core') + private readonly featureFlagRepository: Repository, + @InjectRepository(FieldMetadataEntity, 'metadata') + private readonly fieldMetadataRepository: Repository, + private readonly typeORMService: TypeORMService, + private readonly dataSourceService: DataSourceService, + private readonly workspaceCacheVersionService: WorkspaceCacheVersionService, + private readonly twentyORMManager: TwentyORMManager, + ) { + 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: AddNewAddressFieldToViewsWithDeprecatedAddressFieldCommandOptions, + ): Promise { + // This command can be generic-ified turning the below consts in options + const deprecatedFieldStandardId = + COMPANY_STANDARD_FIELD_IDS.address_deprecated; + const newFieldStandardId = COMPANY_STANDARD_FIELD_IDS.address; + + this.logger.log('running'); + let workspaceIds: string[] = []; + + if (options.workspaceId) { + workspaceIds = [options.workspaceId]; + } else { + const workspaces = await this.workspaceRepository.find(); + + const activeWorkspaceIds = ( + await Promise.all( + workspaces.map(async (workspace) => { + const isActive = await this.workspaceIsActive(workspace); + + return { workspace, isActive }; + }), + ) + ) + .filter((result) => result.isActive) + .map((result) => result.workspace.id); + + 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) { + const viewFieldRepository = + await this.twentyORMManager.getRepositoryForWorkspace( + workspaceId, + ViewFieldWorkspaceEntity, + ); + + const dataSourceMetadatas = + await this.dataSourceService.getDataSourcesMetadataFromWorkspaceId( + workspaceId, + ); + + for (const dataSourceMetadata of dataSourceMetadatas) { + const workspaceDataSource = + await this.typeORMService.connectToDataSource(dataSourceMetadata); + + if (workspaceDataSource) { + try { + const newAddressField = await this.fieldMetadataRepository.findBy({ + workspaceId, + standardId: newFieldStandardId, + }); + + if (isEmpty(newAddressField)) { + this.logger.log( + `Error - missing new Address standard field of type Address, please run workspace-sync-metadata on your workspace (${workspaceId}) before running this command`, + ); + continue; + } + + const addressDeprecatedField = + await this.fieldMetadataRepository.findOneBy({ + workspaceId, + standardId: deprecatedFieldStandardId, + }); + + if (isEmpty(addressDeprecatedField)) { + continue; + } + + const viewsWithAddressDeprecatedField = + await viewFieldRepository.find({ + where: { + fieldMetadataId: addressDeprecatedField.id, + isVisible: true, + }, + }); + + for (const viewWithAddressDeprecatedField of viewsWithAddressDeprecatedField) { + const viewId = viewWithAddressDeprecatedField.viewId; + + const newAddressFieldInThisView = + await viewFieldRepository.findBy({ + fieldMetadataId: newAddressField[0].id, + viewId: viewWithAddressDeprecatedField.viewId as string, + isVisible: true, + }); + + if (!isEmpty(newAddressFieldInThisView)) { + continue; + } + + this.logger.log( + `Adding new address field to view ${viewId} for workspace ${workspaceId}...`, + ); + const newViewField = viewFieldRepository.create({ + viewId: viewWithAddressDeprecatedField.viewId, + fieldMetadataId: newAddressField[0].id, + position: viewWithAddressDeprecatedField.position - 0.5, + isVisible: true, + }); + + await viewFieldRepository.save(newViewField); + this.logger.log( + `New address field successfully added to view ${viewId} for workspace ${workspaceId}`, + ); + } + } catch (error) { + this.logger.log( + chalk.red(`Running command on workspace ${workspaceId} failed`), + ); + throw error; + } + } + } + + await this.workspaceCacheVersionService.incrementVersion(workspaceId); + + this.logger.log( + chalk.green(`Running command on workspace ${workspaceId} done`), + ); + } + + this.logger.log(chalk.green(`Command completed!`)); + } + + private async workspaceIsActive(workspace: Workspace): Promise { + const billingSupscriptionForWorkspace = + await this.billingSubscriptionRepository.findOne({ + where: { workspaceId: workspace.id }, + }); + + if ( + billingSupscriptionForWorkspace?.status && + [ + SubscriptionStatus.PastDue, + SubscriptionStatus.Active, + SubscriptionStatus.Trialing, + ].includes(billingSupscriptionForWorkspace.status as SubscriptionStatus) + ) { + return true; + } + + const freeAccessEnabledFeatureFlagForWorkspace = + await this.featureFlagRepository.findOne({ + where: { + workspaceId: workspace.id, + key: FeatureFlagKeys.IsFreeAccessEnabled, + value: true, + }, + }); + + return !!freeAccessEnabledFeatureFlagForWorkspace; + } +} 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 a3f7ad0dc..1c4286859 100644 --- a/packages/twenty-server/src/database/commands/database-command.module.ts +++ b/packages/twenty-server/src/database/commands/database-command.module.ts @@ -2,6 +2,7 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { UpdateMessageChannelSyncStatusEnumCommand } from 'src/database/commands/0-20-update-message-channel-sync-status-enum.command'; +import { AddNewAddressFieldToViewsWithDeprecatedAddressFieldCommand } from 'src/database/commands/0-22-add-new-address-field-to-views-with-deprecated-address.command'; import { StartDataSeedDemoWorkspaceCronCommand } from 'src/database/commands/data-seed-demo-workspace/crons/start-data-seed-demo-workspace.cron.command'; import { StopDataSeedDemoWorkspaceCronCommand } from 'src/database/commands/data-seed-demo-workspace/crons/stop-data-seed-demo-workspace.cron.command'; import { DataSeedDemoWorkspaceCommand } from 'src/database/commands/data-seed-demo-workspace/data-seed-demo-workspace-command'; @@ -12,6 +13,8 @@ import { UpdateMessageChannelVisibilityEnumCommand } from 'src/database/commands import { UpgradeTo0_22CommandModule } from 'src/database/commands/upgrade-version/0-22/0-22-upgrade-version.module'; import { WorkspaceAddTotalCountCommand } from 'src/database/commands/workspace-add-total-count.command'; 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'; 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'; @@ -28,7 +31,10 @@ import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/worksp WorkspaceManagerModule, DataSourceModule, TypeORMModule, - TypeOrmModule.forFeature([Workspace], 'core'), + TypeOrmModule.forFeature( + [Workspace, BillingSubscription, FeatureFlagEntity], + 'core', + ), TypeOrmModule.forFeature( [FieldMetadataEntity, ObjectMetadataEntity], 'metadata', @@ -52,6 +58,7 @@ import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/worksp StopDataSeedDemoWorkspaceCronCommand, UpdateMessageChannelVisibilityEnumCommand, UpdateMessageChannelSyncStatusEnumCommand, + AddNewAddressFieldToViewsWithDeprecatedAddressFieldCommand, ], }) export class DatabaseCommandModule {} diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/listeners/entity-events-to-db.listener.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/listeners/entity-events-to-db.listener.ts index 3e00d01be..d9a018f86 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/listeners/entity-events-to-db.listener.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/listeners/entity-events-to-db.listener.ts @@ -41,7 +41,7 @@ export class EntityEventsToDbListener { // .... private async handle(payload: ObjectRecordBaseEvent) { - if (!payload.objectMetadata.isAuditLogged) { + if (!payload.objectMetadata?.isAuditLogged) { return; } diff --git a/packages/twenty-server/src/modules/company/standard-objects/company.workspace-entity.ts b/packages/twenty-server/src/modules/company/standard-objects/company.workspace-entity.ts index d356969a3..fbe99fc9f 100644 --- a/packages/twenty-server/src/modules/company/standard-objects/company.workspace-entity.ts +++ b/packages/twenty-server/src/modules/company/standard-objects/company.workspace-entity.ts @@ -9,6 +9,14 @@ import { RelationMetadataType, RelationOnDeleteAction, } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; +import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; +import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator'; +import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator'; +import { WorkspaceIsDeprecated } from 'src/engine/twenty-orm/decorators/workspace-is-deprecated.decorator'; +import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator'; +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 { COMPANY_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 { ActivityTargetWorkspaceEntity } from 'src/modules/activity/standard-objects/activity-target.workspace-entity'; @@ -16,16 +24,8 @@ import { AttachmentWorkspaceEntity } from 'src/modules/attachment/standard-objec import { FavoriteWorkspaceEntity } from 'src/modules/favorite/standard-objects/favorite.workspace-entity'; import { OpportunityWorkspaceEntity } from 'src/modules/opportunity/standard-objects/opportunity.workspace-entity'; import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity'; -import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-objects/timeline-activity.workspace-entity'; -import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; -import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator'; -import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator'; -import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator'; -import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator'; -import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator'; -import { WorkspaceIsDeprecated } from 'src/engine/twenty-orm/decorators/workspace-is-deprecated.decorator'; -import { WorkspaceJoinColumn } from 'src/engine/twenty-orm/decorators/workspace-join-column.decorator'; +import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; @WorkspaceEntity({ standardId: STANDARD_OBJECT_IDS.company, @@ -101,7 +101,7 @@ export class CompanyWorkspaceEntity extends BaseWorkspaceEntity { type: FieldMetadataType.TEXT, label: 'Address (deprecated) ', description: - 'Address of the company - deprecated in favor of new address field', + "This standard field has been deprecated and migrated as a custom field. Please consider using the new 'address' field type.", icon: 'IconMap', }) @WorkspaceIsDeprecated()