mirror of
https://github.com/twentyhq/twenty.git
synced 2024-12-23 20:13:21 +03:00
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 <charles@twenty.com>
This commit is contained in:
parent
c5572f1b1e
commit
7df5f91dc5
@ -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<Workspace>) {
|
||||
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<string[]> {
|
||||
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<void> {
|
||||
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<void>;
|
||||
}
|
46
packages/twenty-server/src/database/commands/base.command.ts
Normal file
46
packages/twenty-server/src/database/commands/base.command.ts
Normal file
@ -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<void> {
|
||||
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<void>;
|
||||
}
|
@ -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<Workspace>,
|
||||
@InjectRepository(ObjectMetadataEntity, 'metadata')
|
||||
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
|
||||
) {
|
||||
super(workspaceRepository);
|
||||
}
|
||||
|
||||
async executeActiveWorkspacesCommand(
|
||||
_passedParam: string[],
|
||||
options: SetCustomObjectIsSoftDeletableCommandOptions,
|
||||
workspaceIds: string[],
|
||||
): Promise<void> {
|
||||
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`);
|
||||
}
|
||||
}
|
@ -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<void> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -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 {}
|
||||
|
@ -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 = {
|
||||
|
@ -17,12 +17,12 @@ export class GraphqlQuerySelectedFieldsRelationParser {
|
||||
fieldValue: any,
|
||||
result: { select: Record<string, any>; relations: Record<string, any> },
|
||||
): void {
|
||||
result.relations[fieldKey] = true;
|
||||
|
||||
if (!fieldValue || typeof fieldValue !== 'object') {
|
||||
return;
|
||||
}
|
||||
|
||||
result.relations[fieldKey] = true;
|
||||
|
||||
const referencedObjectMetadata = getRelationObjectMetadata(
|
||||
fieldMetadata,
|
||||
this.objectMetadataMap,
|
||||
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user