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 5b7b6da83..e3aee52be 100644 --- a/packages/twenty-server/src/database/commands/database-command.module.ts +++ b/packages/twenty-server/src/database/commands/database-command.module.ts @@ -11,6 +11,7 @@ import { UpgradeTo0_32CommandModule } from 'src/database/commands/upgrade-versio import { UpgradeTo0_33CommandModule } from 'src/database/commands/upgrade-version/0-33/0-33-upgrade-version.module'; import { UpgradeTo0_34CommandModule } from 'src/database/commands/upgrade-version/0-34/0-34-upgrade-version.module'; import { UpgradeTo0_35CommandModule } from 'src/database/commands/upgrade-version/0-35/0-35-upgrade-version.module'; +import { UpgradeTo0_40CommandModule } from 'src/database/commands/upgrade-version/0-40/0-40-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'; @@ -55,6 +56,7 @@ import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/worksp UpgradeTo0_33CommandModule, UpgradeTo0_34CommandModule, UpgradeTo0_35CommandModule, + UpgradeTo0_40CommandModule, FeatureFlagModule, ], providers: [ diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-35/0-35-upgrade-version.command.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-35/0-35-upgrade-version.command.ts index 7f0d7cdb0..a7ee898f0 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version/0-35/0-35-upgrade-version.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version/0-35/0-35-upgrade-version.command.ts @@ -35,7 +35,7 @@ export class UpgradeTo0_35Command extends ActiveWorkspacesCommandRunner { workspaceIds: string[], ): Promise { this.logger.log( - 'Running command to upgrade to 0.40: must start with phone calling code otherwise SyncMetadata will fail', + 'Running command to upgrade to 0.35: must start with phone calling code otherwise SyncMetadata will fail', ); await this.recordPositionBackfillCommand.executeActiveWorkspacesCommand( diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-40/0-40-migrate-aggregate-operations-options.command.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-40/0-40-migrate-aggregate-operations-options.command.ts new file mode 100644 index 000000000..26bdf699d --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version/0-40/0-40-migrate-aggregate-operations-options.command.ts @@ -0,0 +1,245 @@ +import { InjectRepository } from '@nestjs/typeorm'; + +import chalk from 'chalk'; +import { Command } from 'nest-commander'; +import { Repository } from 'typeorm'; +import { v4 } from 'uuid'; + +import { + ActiveWorkspacesCommandOptions, + ActiveWorkspacesCommandRunner, +} from 'src/database/commands/active-workspaces.command'; +import { isCommandLogger } from 'src/database/commands/logger'; +import { AGGREGATE_OPERATIONS } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +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/services/workspace-metadata-version.service'; +import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util'; +import { + WorkspaceMigrationColumnActionType, + WorkspaceMigrationTableAction, + WorkspaceMigrationTableActionType, +} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; +import { WorkspaceMigrationFactory } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.factory'; +import { WorkspaceMigrationService } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.service'; +import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; +import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service'; +import { + VIEW_FIELD_STANDARD_FIELD_IDS, + VIEW_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 { isDefined } from 'src/utils/is-defined'; + +@Command({ + name: 'upgrade-0.40:migrate-aggregate-operation-options', + description: 'Add aggregate operations options to relevant fields', +}) +export class MigrateAggregateOperationOptionsCommand extends ActiveWorkspacesCommandRunner { + constructor( + @InjectRepository(Workspace, 'core') + protected readonly workspaceRepository: Repository, + @InjectRepository(FieldMetadataEntity, 'metadata') + private readonly fieldMetadataRepository: Repository, + @InjectRepository(ObjectMetadataEntity, 'metadata') + private readonly objectMetadataRepository: Repository, + private readonly workspaceMigrationService: WorkspaceMigrationService, + private readonly workspaceMigrationFactory: WorkspaceMigrationFactory, + private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService, + private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService, + ) { + super(workspaceRepository); + } + + ADDITIONAL_AGGREGATE_OPERATIONS = [ + { + value: AGGREGATE_OPERATIONS.countEmpty, + label: 'Count empty', + position: 5, + color: 'red', + }, + { + value: AGGREGATE_OPERATIONS.countNotEmpty, + label: 'Count not empty', + position: 6, + color: 'purple', + }, + { + value: AGGREGATE_OPERATIONS.countUniqueValues, + label: 'Count unique values', + position: 7, + color: 'sky', + }, + { + value: AGGREGATE_OPERATIONS.percentageEmpty, + label: 'Percent empty', + position: 8, + color: 'turquoise', + }, + { + value: AGGREGATE_OPERATIONS.percentageNotEmpty, + label: 'Percent not empty', + position: 9, + color: 'yellow', + }, + ]; + + ADDITIONAL_AGGREGATE_OPERATIONS_VALUES = + this.ADDITIONAL_AGGREGATE_OPERATIONS.map((option) => option.value); + + async executeActiveWorkspacesCommand( + _passedParam: string[], + options: ActiveWorkspacesCommandOptions, + workspaceIds: string[], + ): Promise { + this.logger.log( + 'Running command to migrate aggregate operations options to include count operations', + ); + + if (isCommandLogger(this.logger)) { + this.logger.setVerbose(options.verbose ?? false); + } + + let workspaceIterator = 1; + + for (const workspaceId of workspaceIds) { + this.logger.log( + `Running command for workspace ${workspaceId} ${workspaceIterator}/${workspaceIds.length}`, + ); + + try { + const viewFieldObjectMetadata = + await this.objectMetadataRepository.findOne({ + where: { + workspaceId, + standardId: STANDARD_OBJECT_IDS.viewField, + }, + }); + + if (!isDefined(viewFieldObjectMetadata)) { + throw new Error( + `View field object metadata not found for workspace ${workspaceId}`, + ); + } + + const viewFieldAggregateOperationFieldMetadata = + await this.fieldMetadataRepository.findOne({ + where: { + workspaceId, + objectMetadataId: viewFieldObjectMetadata.id, + standardId: VIEW_FIELD_STANDARD_FIELD_IDS.aggregateOperation, + }, + }); + + if (isDefined(viewFieldAggregateOperationFieldMetadata)) { + await this.updateAggregateOperationField( + workspaceId, + viewFieldAggregateOperationFieldMetadata, + viewFieldObjectMetadata, + ); + } + + const viewObjectMetadata = await this.objectMetadataRepository.findOne({ + where: { + workspaceId, + standardId: STANDARD_OBJECT_IDS.view, + }, + }); + + if (!isDefined(viewObjectMetadata)) { + throw new Error( + `View object metadata not found for workspace ${workspaceId}`, + ); + } + + const viewAggregateOperationFieldMetadata = + await this.fieldMetadataRepository.findOne({ + where: { + workspaceId, + objectMetadataId: viewObjectMetadata.id, + standardId: VIEW_STANDARD_FIELD_IDS.kanbanAggregateOperation, + }, + }); + + if (isDefined(viewAggregateOperationFieldMetadata)) { + await this.updateAggregateOperationField( + workspaceId, + viewAggregateOperationFieldMetadata, + viewObjectMetadata, + ); + } + + if ( + isDefined(viewAggregateOperationFieldMetadata) || + isDefined(viewFieldAggregateOperationFieldMetadata) + ) { + await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations( + workspaceId, + ); + + await this.workspaceMetadataVersionService.incrementMetadataVersion( + workspaceId, + ); + } + workspaceIterator++; + this.logger.log( + chalk.green(`Command completed for workspace ${workspaceId}.`), + ); + } catch { + this.logger.log(chalk.red(`Error in workspace ${workspaceId}.`)); + workspaceIterator++; + } + } + this.logger.log(chalk.green(`Command completed!`)); + } + + private async updateAggregateOperationField( + workspaceId: string, + fieldMetadata: FieldMetadataEntity, + objectMetadata: ObjectMetadataEntity, + ) { + if ( + fieldMetadata.options.some((option) => { + return this.ADDITIONAL_AGGREGATE_OPERATIONS_VALUES.includes( + option.value as AGGREGATE_OPERATIONS, + ); + }) + ) { + this.logger.log( + `Aggregate operation field metadata ${fieldMetadata.name} already has the required options`, + ); + } else { + const updatedFieldMetadata = { + ...fieldMetadata, + options: [ + ...fieldMetadata.options, + ...this.ADDITIONAL_AGGREGATE_OPERATIONS.map((operation) => ({ + ...operation, + id: v4(), + })), + ], + }; + + await this.fieldMetadataRepository.save(updatedFieldMetadata); + + await this.workspaceMigrationService.createCustomMigration( + generateMigrationName( + `update-${objectMetadata.nameSingular}-aggregate-operation`, + ), + workspaceId, + [ + { + name: computeObjectTargetTable(objectMetadata), + action: WorkspaceMigrationTableActionType.ALTER, + columns: this.workspaceMigrationFactory.createColumnActions( + WorkspaceMigrationColumnActionType.ALTER, + fieldMetadata, + updatedFieldMetadata, + ), + } satisfies WorkspaceMigrationTableAction, + ], + ); + } + } +} diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-40/0-40-upgrade-version.command.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-40/0-40-upgrade-version.command.ts new file mode 100644 index 000000000..11f965a83 --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version/0-40/0-40-upgrade-version.command.ts @@ -0,0 +1,37 @@ +import { InjectRepository } from '@nestjs/typeorm'; + +import { Command } from 'nest-commander'; +import { Repository } from 'typeorm'; + +import { ActiveWorkspacesCommandRunner } from 'src/database/commands/active-workspaces.command'; +import { BaseCommandOptions } from 'src/database/commands/base.command'; +import { MigrateAggregateOperationOptionsCommand } from 'src/database/commands/upgrade-version/0-40/0-40-migrate-aggregate-operations-options.command'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; + +@Command({ + name: 'upgrade-0.40', + description: 'Upgrade to 0.40', +}) +export class UpgradeTo0_40Command extends ActiveWorkspacesCommandRunner { + constructor( + @InjectRepository(Workspace, 'core') + protected readonly workspaceRepository: Repository, + private readonly migrateAggregateOperationOptionsCommand: MigrateAggregateOperationOptionsCommand, + ) { + super(workspaceRepository); + } + + async executeActiveWorkspacesCommand( + passedParam: string[], + options: BaseCommandOptions, + workspaceIds: string[], + ): Promise { + this.logger.log('Running command to upgrade to 0.40'); + + await this.migrateAggregateOperationOptionsCommand.executeActiveWorkspacesCommand( + passedParam, + options, + workspaceIds, + ); + } +} diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-40/0-40-upgrade-version.module.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-40/0-40-upgrade-version.module.ts new file mode 100644 index 000000000..cc33bd878 --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version/0-40/0-40-upgrade-version.module.ts @@ -0,0 +1,26 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +import { MigrateAggregateOperationOptionsCommand } from 'src/database/commands/upgrade-version/0-40/0-40-migrate-aggregate-operations-options.command'; +import { UpgradeTo0_40Command } from 'src/database/commands/upgrade-version/0-40/0-40-upgrade-version.command'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +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 { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module'; +import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.module'; +import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.module'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([Workspace], 'core'), + TypeOrmModule.forFeature( + [ObjectMetadataEntity, FieldMetadataEntity], + 'metadata', + ), + WorkspaceMigrationRunnerModule, + WorkspaceMigrationModule, + WorkspaceMetadataVersionModule, + ], + providers: [UpgradeTo0_40Command, MigrateAggregateOperationOptionsCommand], +}) +export class UpgradeTo0_40CommandModule {}