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()