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 000000000000..d741144cb8ea --- /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 000000000000..6715b5c293bd --- /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 000000000000..a27a3cc144eb --- /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 08b32b05e235..2862f566a34e 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 b718b9ed249c..689938ba3468 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 7aa3ed1ab77d..de67cc736796 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 4d22099219ac..9c253b184510 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 152d44e8ef6d..49c3b95d75cd 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, ); }