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