From 7df5f91dc5925e050fc3ba1423195f7dfb9c2356 Mon Sep 17 00:00:00 2001 From: Weiko Date: Sat, 31 Aug 2024 17:49:12 +0200 Subject: [PATCH] Add set custom object is soft deletable command (#6788) ## Context Custom object were not automatically created as softDeletable, this has been fixed in a recent PR. This PR adds a command to backfill existing custom objects. We also introduce a baseCommandRunner and ActiveWorkspacesCommandRunner to put some boilerplate and simplify future commands. ## Test ```bash yarn command:prod upgrade-0-24:set-custom-object-is-soft-deletable [Nest] 75852 - 08/29/2024, 5:16:41 PM LOG [SetCustomObjectIsSoftDeletableCommand] Running command on 2 workspaces query: UPDATE "metadata"."objectMetadata" SET "isSoftDeletable" = $1, "updatedAt" = CURRENT_TIMESTAMP WHERE ("workspaceId" IN ($2, $3) AND "isCustom" = $4 AND "isSoftDeletable" = $5) -- PARAMETERS: [true,"3b8e6458-5fc1-4e63-8563-008ccddaa6db","20202020-1c25-4d02-bf25-6aeccf7ea419",true,false] [Nest] 75852 - 08/29/2024, 5:16:41 PM LOG [SetCustomObjectIsSoftDeletableCommand] Updated 1 entities [Nest] 75852 - 08/29/2024, 5:16:41 PM LOG [SetCustomObjectIsSoftDeletableCommand] Command completed! ``` ```bash yarn command:prod upgrade-0-24:set-custom-object-is-soft-deletable -d [Nest] 75424 - 08/29/2024, 5:16:14 PM LOG [SetCustomObjectIsSoftDeletableCommand] Running command on 2 workspaces [Nest] 75424 - 08/29/2024, 5:16:14 PM LOG [SetCustomObjectIsSoftDeletableCommand] Dry run mode: No changes will be applied query: SELECT "ObjectMetadataEntity"."id" AS "ObjectMetadataEntity_id" FROM "metadata"."objectMetadata" "ObjectMetadataEntity" WHERE (("ObjectMetadataEntity"."workspaceId" IN ($1, $2)) AND ("ObjectMetadataEntity"."isCustom" = $3) AND ("ObjectMetadataEntity"."isSoftDeletable" = $4)) -- PARAMETERS: ["3b8e6458-5fc1-4e63-8563-008ccddaa6db","20202020-1c25-4d02-bf25-6aeccf7ea419",true,false] [Nest] 75424 - 08/29/2024, 5:16:14 PM LOG [SetCustomObjectIsSoftDeletableCommand] Dry run: 1 entities would be updated [Nest] 75424 - 08/29/2024, 5:16:14 PM LOG [SetCustomObjectIsSoftDeletableCommand] Command completed! ``` ```bash yarn command:prod upgrade-0-24:set-custom-object-is-soft-deletable -w 20202020-1c25-4d02-bf25-6aeccf7ea419 -w 20202020-1c25-4d02-bf25-6aeccf7ea419 query: UPDATE "metadata"."objectMetadata" SET "isSoftDeletable" = $1, "updatedAt" = CURRENT_TIMESTAMP WHERE ("workspaceId" IN ($2, $3) AND "isCustom" = $4) -- PARAMETERS: [true,"20202020-1c25-4d02-bf25-6aeccf7ea419","20202020-1c25-4d02-bf25-6aeccf7ea419",true] [Nest] 70588 - 08/29/2024, 5:11:31 PM LOG [SetCustomObjectIsSoftDeletableCommand] Updated 2 entities [Nest] 70588 - 08/29/2024, 5:11:31 PM LOG [SetCustomObjectIsSoftDeletableCommand] Command completed! ``` --------- Co-authored-by: Charles Bochet --- .../commands/active-workspaces.command.ts | 92 +++++++++++++++++++ .../src/database/commands/base.command.ts | 46 ++++++++++ ...custom-object-is-soft-deletable.command.ts | 60 ++++++++++++ .../0-24/0-24-upgrade-version.command.ts | 9 +- .../0-24/0-24-upgrade-version.module.ts | 7 +- .../typeorm/metadata/metadata.datasource.ts | 2 +- ...graphql-selected-fields-relation.parser.ts | 4 +- .../graphql-query-runner.service.ts | 5 +- 8 files changed, 216 insertions(+), 9 deletions(-) create mode 100644 packages/twenty-server/src/database/commands/active-workspaces.command.ts create mode 100644 packages/twenty-server/src/database/commands/base.command.ts create mode 100644 packages/twenty-server/src/database/commands/upgrade-version/0-24/0-24-set-custom-object-is-soft-deletable.command.ts diff --git a/packages/twenty-server/src/database/commands/active-workspaces.command.ts b/packages/twenty-server/src/database/commands/active-workspaces.command.ts new file mode 100644 index 000000000..d741144cb --- /dev/null +++ b/packages/twenty-server/src/database/commands/active-workspaces.command.ts @@ -0,0 +1,92 @@ +import { Logger } from '@nestjs/common'; + +import chalk from 'chalk'; +import { Option } from 'nest-commander'; +import { Repository } from 'typeorm'; + +import { + BaseCommandOptions, + BaseCommandRunner, +} from 'src/database/commands/base.command'; +import { + Workspace, + WorkspaceActivationStatus, +} from 'src/engine/core-modules/workspace/workspace.entity'; + +export type ActiveWorkspacesCommandOptions = BaseCommandOptions & { + workspaceId?: string; +}; + +export abstract class ActiveWorkspacesCommandRunner extends BaseCommandRunner { + private workspaceIds: string[] = []; + + protected readonly logger: Logger; + + constructor(protected readonly workspaceRepository: Repository) { + super(); + this.logger = new Logger(this.constructor.name); + } + + @Option({ + flags: '-w, --workspace-id [workspace_id]', + description: + 'workspace id. Command runs on all active workspaces if not provided', + required: false, + }) + parseWorkspaceId(val: string): string[] { + this.workspaceIds.push(val); + + return this.workspaceIds; + } + + protected async fetchActiveWorkspaceIds(): Promise { + const activeWorkspaces = await this.workspaceRepository.find({ + select: ['id'], + where: { + activationStatus: WorkspaceActivationStatus.ACTIVE, + }, + }); + + return activeWorkspaces.map((workspace) => workspace.id); + } + + protected logWorkspaceCount(activeWorkspaceIds: string[]): void { + if (!activeWorkspaceIds.length) { + this.logger.log(chalk.yellow('No workspace found')); + } else { + this.logger.log( + chalk.green( + `Running command on ${activeWorkspaceIds.length} workspaces`, + ), + ); + } + } + + override async executeBaseCommand( + passedParams: string[], + options: BaseCommandOptions, + ): Promise { + const activeWorkspaceIds = + this.workspaceIds.length > 0 + ? this.workspaceIds + : await this.fetchActiveWorkspaceIds(); + + this.logWorkspaceCount(activeWorkspaceIds); + + if (options.dryRun) { + this.logger.log(chalk.yellow('Dry run mode: No changes will be applied')); + } + + await this.executeActiveWorkspacesCommand( + passedParams, + options, + activeWorkspaceIds, + ); + } + + protected abstract executeActiveWorkspacesCommand( + passedParams: string[], + options: BaseCommandOptions, + activeWorkspaceIds: string[], + ): Promise; +} diff --git a/packages/twenty-server/src/database/commands/base.command.ts b/packages/twenty-server/src/database/commands/base.command.ts new file mode 100644 index 000000000..6715b5c29 --- /dev/null +++ b/packages/twenty-server/src/database/commands/base.command.ts @@ -0,0 +1,46 @@ +import { Logger } from '@nestjs/common'; + +import chalk from 'chalk'; +import { CommandRunner, Option } from 'nest-commander'; + +export type BaseCommandOptions = { + workspaceId?: string; + dryRun?: boolean; +}; + +export abstract class BaseCommandRunner extends CommandRunner { + protected readonly logger: Logger; + + constructor() { + super(); + this.logger = new Logger(this.constructor.name); + } + + @Option({ + flags: '-d, --dry-run', + description: 'Simulate the command without making actual changes', + required: false, + }) + parseDryRun(): boolean { + return true; + } + + override async run( + passedParams: string[], + options: BaseCommandOptions, + ): Promise { + try { + await this.executeBaseCommand(passedParams, options); + } catch (error) { + this.logger.error(chalk.red(`Command failed`)); + throw error; + } finally { + this.logger.log(chalk.blue('Command completed!')); + } + } + + protected abstract executeBaseCommand( + passedParams: string[], + options: BaseCommandOptions, + ): Promise; +} diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-24/0-24-set-custom-object-is-soft-deletable.command.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-24/0-24-set-custom-object-is-soft-deletable.command.ts new file mode 100644 index 000000000..a27a3cc14 --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version/0-24/0-24-set-custom-object-is-soft-deletable.command.ts @@ -0,0 +1,60 @@ +import { InjectRepository } from '@nestjs/typeorm'; + +import { Command } from 'nest-commander'; +import { In, Repository } from 'typeorm'; + +import { + ActiveWorkspacesCommandOptions, + ActiveWorkspacesCommandRunner, +} from 'src/database/commands/active-workspaces.command'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; + +type SetCustomObjectIsSoftDeletableCommandOptions = + ActiveWorkspacesCommandOptions; + +@Command({ + name: 'upgrade-0.24:set-custom-object-is-soft-deletable', + description: 'Set custom object is soft deletable', +}) +export class SetCustomObjectIsSoftDeletableCommand extends ActiveWorkspacesCommandRunner { + constructor( + @InjectRepository(Workspace, 'core') + protected readonly workspaceRepository: Repository, + @InjectRepository(ObjectMetadataEntity, 'metadata') + private readonly objectMetadataRepository: Repository, + ) { + super(workspaceRepository); + } + + async executeActiveWorkspacesCommand( + _passedParam: string[], + options: SetCustomObjectIsSoftDeletableCommandOptions, + workspaceIds: string[], + ): Promise { + const updateCriteria = { + workspaceId: In(workspaceIds), + isCustom: true, + isSoftDeletable: false, + }; + + if (options.dryRun) { + const objectsToUpdate = await this.objectMetadataRepository.find({ + select: ['id'], + where: updateCriteria, + }); + + this.logger.log( + `Dry run: ${objectsToUpdate.length} objects would be updated`, + ); + + return; + } + + const result = await this.objectMetadataRepository.update(updateCriteria, { + isSoftDeletable: true, + }); + + this.logger.log(`Updated ${result.affected} objects`); + } +} 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 index 08b32b05e..2862f566a 100644 --- 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 @@ -1,5 +1,6 @@ import { Command, CommandRunner, Option } from 'nest-commander'; +import { SetCustomObjectIsSoftDeletableCommand } from 'src/database/commands/upgrade-version/0-24/0-24-set-custom-object-is-soft-deletable.command'; 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'; @@ -15,6 +16,7 @@ export class UpgradeTo0_24Command extends CommandRunner { constructor( private readonly syncWorkspaceMetadataCommand: SyncWorkspaceMetadataCommand, private readonly setMessagesDirectionCommand: SetMessageDirectionCommand, + private readonly setCustomObjectIsSoftDeletableCommand: SetCustomObjectIsSoftDeletableCommand, ) { super(); } @@ -30,13 +32,14 @@ export class UpgradeTo0_24Command extends CommandRunner { } async run( - _passedParam: string[], + passedParam: string[], options: UpdateTo0_24CommandOptions, ): Promise { - await this.syncWorkspaceMetadataCommand.run(_passedParam, { + await this.syncWorkspaceMetadataCommand.run(passedParam, { ...options, force: true, }); - await this.setMessagesDirectionCommand.run(_passedParam, options); + await this.setMessagesDirectionCommand.run(passedParam, options); + await this.setCustomObjectIsSoftDeletableCommand.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 index b718b9ed2..689938ba3 100644 --- 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 @@ -1,6 +1,7 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; +import { SetCustomObjectIsSoftDeletableCommand } from 'src/database/commands/upgrade-version/0-24/0-24-set-custom-object-is-soft-deletable.command'; 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'; @@ -33,6 +34,10 @@ import { WorkspaceSyncMetadataCommandsModule } from 'src/engine/workspace-manage ), TypeORMModule, ], - providers: [UpgradeTo0_24Command, SetMessageDirectionCommand], + providers: [ + UpgradeTo0_24Command, + SetMessageDirectionCommand, + SetCustomObjectIsSoftDeletableCommand, + ], }) export class UpgradeTo0_24CommandModule {} diff --git a/packages/twenty-server/src/database/typeorm/metadata/metadata.datasource.ts b/packages/twenty-server/src/database/typeorm/metadata/metadata.datasource.ts index 7aa3ed1ab..de67cc736 100644 --- a/packages/twenty-server/src/database/typeorm/metadata/metadata.datasource.ts +++ b/packages/twenty-server/src/database/typeorm/metadata/metadata.datasource.ts @@ -1,7 +1,7 @@ import { TypeOrmModuleOptions } from '@nestjs/typeorm'; -import { DataSource, DataSourceOptions } from 'typeorm'; import { config } from 'dotenv'; +import { DataSource, DataSourceOptions } from 'typeorm'; config(); export const typeORMMetadataModuleOptions: TypeOrmModuleOptions = { diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields-relation.parser.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields-relation.parser.ts index 4d2209921..9c253b184 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields-relation.parser.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields-relation.parser.ts @@ -17,12 +17,12 @@ export class GraphqlQuerySelectedFieldsRelationParser { fieldValue: any, result: { select: Record; relations: Record }, ): void { - result.relations[fieldKey] = true; - if (!fieldValue || typeof fieldValue !== 'object') { return; } + result.relations[fieldKey] = true; + const referencedObjectMetadata = getRelationObjectMetadata( fieldMetadata, this.objectMetadataMap, diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service.ts index 152d44e8e..49c3b95d7 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service.ts @@ -58,8 +58,9 @@ export class GraphqlQueryRunnerService { const objectMetadata = objectMetadataMap[objectMetadataItem.nameSingular]; if (!objectMetadata) { - throw new Error( - `Object metadata for ${objectMetadataItem.nameSingular} not found`, + throw new GraphqlQueryRunnerException( + `Object metadata not found for ${objectMetadataItem.nameSingular}`, + GraphqlQueryRunnerExceptionCode.OBJECT_METADATA_NOT_FOUND, ); }