Refactor upgrade commands (#10592)

Simplifying a lot the upgrade system.

New way to upgrade:
`yarn command:prod upgrade`

New way to write upgrade commands (all wrapping is done for you)
```
  override async runOnWorkspace({
    index,
    total,
    workspaceId,
    options,
  }: RunOnWorkspaceArgs): Promise<void> {}
```

Also cleaning CommandModule imports to make it lighter
This commit is contained in:
Charles Bochet
2025-02-28 19:51:32 +01:00
committed by GitHub
parent 194b5889fe
commit baa3043954
44 changed files with 714 additions and 2212 deletions

View File

@ -0,0 +1,156 @@
import chalk from 'chalk';
import { Option } from 'nest-commander';
import { WorkspaceActivationStatus } from 'twenty-shared';
import { In, MoreThanOrEqual, Repository } from 'typeorm';
import { MigrationCommandRunner } from 'src/database/commands/command-runners/migration.command-runner';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
export type ActiveOrSuspendedWorkspacesMigrationCommandOptions = {
workspaceIds: string[];
startFromWorkspaceId?: string;
workspaceCountLimit?: number;
dryRun?: boolean;
verbose?: boolean;
};
export type RunOnWorkspaceArgs = {
options: ActiveOrSuspendedWorkspacesMigrationCommandOptions;
workspaceId: string;
dataSource: WorkspaceDataSource;
index: number;
total: number;
};
export abstract class ActiveOrSuspendedWorkspacesMigrationCommandRunner<
Options extends
ActiveOrSuspendedWorkspacesMigrationCommandOptions = ActiveOrSuspendedWorkspacesMigrationCommandOptions,
> extends MigrationCommandRunner {
private workspaceIds: string[] = [];
private startFromWorkspaceId: string | undefined;
private workspaceCountLimit: number | undefined;
constructor(
protected readonly workspaceRepository: Repository<Workspace>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
) {
super();
}
@Option({
flags: '--start-from-workspace-id [workspace_id]',
description:
'Start from a specific workspace id. Workspaces are processed in ascending order of id.',
required: false,
})
parseStartFromWorkspaceId(val: string): string {
this.startFromWorkspaceId = val;
return val;
}
@Option({
flags: '--workspace-count-limit [count]',
description:
'Limit the number of workspaces to process. Workspaces are processed in ascending order of id.',
required: false,
})
parseWorkspaceCountLimit(val: string): number {
this.workspaceCountLimit = parseInt(val);
if (isNaN(this.workspaceCountLimit)) {
throw new Error('Workspace count limit must be a number');
}
if (this.workspaceCountLimit <= 0) {
throw new Error('Workspace count limit must be greater than 0');
}
return this.workspaceCountLimit;
}
@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: In([
WorkspaceActivationStatus.ACTIVE,
WorkspaceActivationStatus.SUSPENDED,
]),
...(this.startFromWorkspaceId
? { id: MoreThanOrEqual(this.startFromWorkspaceId) }
: {}),
},
order: {
id: 'ASC',
},
take: this.workspaceCountLimit,
});
return activeWorkspaces.map((workspace) => workspace.id);
}
override async runMigrationCommand(
_passedParams: string[],
options: Options,
): Promise<void> {
const activeWorkspaceIds =
this.workspaceIds.length > 0
? this.workspaceIds
: await this.fetchActiveWorkspaceIds();
if (options.dryRun) {
this.logger.log(chalk.yellow('Dry run mode: No changes will be applied'));
}
try {
for (const [index, workspaceId] of activeWorkspaceIds.entries()) {
this.logger.log(
`Running command on workspace ${workspaceId} ${index + 1}/${activeWorkspaceIds.length}`,
);
const dataSource =
await this.twentyORMGlobalManager.getDataSourceForWorkspace(
workspaceId,
false,
);
try {
await this.runOnWorkspace({
options,
workspaceId,
dataSource,
index: index,
total: activeWorkspaceIds.length,
});
} catch (error) {
this.logger.warn(
chalk.red(`Error in workspace ${workspaceId}: ${error.message}`),
);
}
await this.twentyORMGlobalManager.destroyDataSourceForWorkspace(
workspaceId,
);
}
} catch (error) {
this.logger.error(error);
}
}
protected abstract runOnWorkspace(args: RunOnWorkspaceArgs): Promise<void>;
}

View File

@ -0,0 +1,67 @@
import { Logger } from '@nestjs/common';
import chalk from 'chalk';
import { CommandRunner, Option } from 'nest-commander';
import { CommandLogger } from 'src/database/commands/logger';
export type MigrationCommandOptions = {
dryRun?: boolean;
verbose?: boolean;
};
export abstract class MigrationCommandRunner extends CommandRunner {
protected logger: CommandLogger | Logger;
constructor() {
super();
this.logger = new CommandLogger({
verbose: false,
constructorName: this.constructor.name,
});
}
@Option({
flags: '-d, --dry-run',
description: 'Simulate the command without making actual changes',
required: false,
})
parseDryRun(): boolean {
return true;
}
@Option({
flags: '-v, --verbose',
description: 'Verbose output',
required: false,
})
parseVerbose(): boolean {
return true;
}
override async run(
passedParams: string[],
options: MigrationCommandOptions,
): Promise<void> {
if (options.verbose) {
this.logger = new CommandLogger({
verbose: true,
constructorName: this.constructor.name,
});
}
try {
await this.runMigrationCommand(passedParams, options);
} catch (error) {
this.logger.error(chalk.red(`Command failed`));
throw error;
} finally {
this.logger.log(chalk.blue('Command completed!'));
}
}
protected abstract runMigrationCommand(
passedParams: string[],
options: MigrationCommandOptions,
): Promise<void>;
}