# Introduction Related https://github.com/twentyhq/core-team-issues/issues/1910 From now on the upgrade won't integrate any sync metadata as it's going to be deprecated very soon Any updates to about to removes workspace-entity or standard flat entity will require a dedicated upgrade command, what we've already started doing during the 1.13 sprint, until we have totally migrated the v2 to be workspace agnostic
334 lines
11 KiB
TypeScript
334 lines
11 KiB
TypeScript
import { InjectRepository } from '@nestjs/typeorm';
|
|
|
|
import { exec } from 'child_process';
|
|
import { promisify } from 'util';
|
|
|
|
import chalk from 'chalk';
|
|
import { SemVer } from 'semver';
|
|
import { isDefined } from 'twenty-shared/utils';
|
|
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
|
|
import { In, Repository } from 'typeorm';
|
|
|
|
import {
|
|
ActiveOrSuspendedWorkspacesMigrationCommandOptions,
|
|
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
|
|
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
|
import {
|
|
RunOnWorkspaceArgs,
|
|
WorkspacesMigrationCommandRunner,
|
|
} from 'src/database/commands/command-runners/workspaces-migration.command-runner';
|
|
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
|
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
|
import { type DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
|
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
|
import {
|
|
type CompareVersionMajorAndMinorReturnType,
|
|
compareVersionMajorAndMinor,
|
|
} from 'src/utils/version/compare-version-minor-and-major';
|
|
import { getPreviousVersion } from 'src/utils/version/get-previous-version';
|
|
|
|
export type VersionCommands = (
|
|
| WorkspacesMigrationCommandRunner
|
|
| ActiveOrSuspendedWorkspacesMigrationCommandRunner
|
|
)[];
|
|
export type AllCommands = Record<string, VersionCommands>;
|
|
const execPromise = promisify(exec);
|
|
|
|
export abstract class UpgradeCommandRunner extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
|
private fromWorkspaceVersion: SemVer;
|
|
private currentAppVersion: SemVer;
|
|
public abstract allCommands: AllCommands;
|
|
public commands: VersionCommands;
|
|
public readonly VALIDATE_WORKSPACE_VERSION_FEATURE_FLAG?: true;
|
|
|
|
constructor(
|
|
@InjectRepository(WorkspaceEntity)
|
|
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
|
protected readonly twentyConfigService: TwentyConfigService,
|
|
protected readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
|
|
protected readonly dataSourceService: DataSourceService,
|
|
) {
|
|
super(workspaceRepository, globalWorkspaceOrmManager, dataSourceService);
|
|
}
|
|
|
|
private async loadActiveOrSuspendedWorkspace() {
|
|
return await this.workspaceRepository.find({
|
|
select: ['id', 'version', 'displayName'],
|
|
where: {
|
|
activationStatus: In([
|
|
WorkspaceActivationStatus.ACTIVE,
|
|
WorkspaceActivationStatus.SUSPENDED,
|
|
]),
|
|
},
|
|
order: {
|
|
id: 'ASC',
|
|
},
|
|
});
|
|
}
|
|
|
|
private async shouldSkipUpgradeIfFreshInstallation(): Promise<boolean> {
|
|
const activeWorkspaceOrSuspendedWorkspaceCount =
|
|
await this.loadActiveOrSuspendedWorkspace();
|
|
|
|
return activeWorkspaceOrSuspendedWorkspaceCount.length === 0;
|
|
}
|
|
|
|
async runCoreMigrations(): Promise<void> {
|
|
this.logger.log('Running global database migrations');
|
|
|
|
try {
|
|
this.logger.log('Running core datasource migrations...');
|
|
const coreResult = await execPromise(
|
|
'npx -y typeorm migration:run -d dist/database/typeorm/core/core.datasource',
|
|
);
|
|
|
|
this.logger.log(coreResult.stdout);
|
|
|
|
this.logger.log('Database migrations completed successfully');
|
|
} catch (error) {
|
|
this.logger.error('Error running database migrations:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
private async workspacesThatAreBelowFromWorkspaceVersion(
|
|
fromWorkspaceVersion: SemVer,
|
|
): Promise<Pick<WorkspaceEntity, 'id' | 'displayName' | 'version'>[]> {
|
|
try {
|
|
const allActiveOrSuspendedWorkspaces =
|
|
await this.loadActiveOrSuspendedWorkspace();
|
|
|
|
if (allActiveOrSuspendedWorkspaces.length === 0) {
|
|
this.logger.log(
|
|
'No workspaces found. Running migrations for fresh installation.',
|
|
);
|
|
|
|
return [];
|
|
}
|
|
|
|
const workspacesThatAreBelowFromWorkspaceVersion =
|
|
allActiveOrSuspendedWorkspaces.filter((workspace) => {
|
|
if (!isDefined(workspace.version)) {
|
|
return true;
|
|
}
|
|
|
|
try {
|
|
const versionCompareResult = compareVersionMajorAndMinor(
|
|
workspace.version,
|
|
fromWorkspaceVersion.version,
|
|
);
|
|
|
|
return versionCompareResult === 'lower';
|
|
} catch (error) {
|
|
this.logger.error(
|
|
`Error checking workspace ${workspace.id} version: ${error.message}`,
|
|
);
|
|
|
|
return true;
|
|
}
|
|
});
|
|
|
|
return workspacesThatAreBelowFromWorkspaceVersion;
|
|
} catch (error) {
|
|
this.logger.error('Error checking workspaces below version:', error);
|
|
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
private setUpgradeContextVersionsAndCommandsForCurrentAppVersion() {
|
|
const upgradeContextIsAlreadyDefined = [
|
|
this.currentAppVersion,
|
|
this.commands,
|
|
this.fromWorkspaceVersion,
|
|
].every(isDefined);
|
|
|
|
if (upgradeContextIsAlreadyDefined) {
|
|
return;
|
|
}
|
|
|
|
const currentAppVersion = this.retrieveCurrentAppVersion();
|
|
const currentVersionMajorMinor = `${currentAppVersion.major}.${currentAppVersion.minor}.0`;
|
|
const currentCommands = this.allCommands[currentVersionMajorMinor];
|
|
|
|
if (!isDefined(currentCommands)) {
|
|
throw new Error(
|
|
`No command found for version ${currentAppVersion}. Please check the commands record.`,
|
|
);
|
|
}
|
|
|
|
const allCommandsVersions = Object.keys(this.allCommands);
|
|
const previousVersion = getPreviousVersion({
|
|
currentVersion: currentVersionMajorMinor,
|
|
versions: allCommandsVersions,
|
|
});
|
|
|
|
if (!isDefined(previousVersion)) {
|
|
throw new Error(
|
|
`No previous version found for version ${currentAppVersion}. Please review the "allCommands" record. Available versions are: ${allCommandsVersions.join(', ')}`,
|
|
);
|
|
}
|
|
this.commands = currentCommands;
|
|
this.fromWorkspaceVersion = previousVersion;
|
|
this.currentAppVersion = currentAppVersion;
|
|
|
|
const message = [
|
|
'Initialized upgrade context with:',
|
|
`- currentVersion (migrating to): ${currentAppVersion}`,
|
|
`- fromWorkspaceVersion: ${previousVersion}`,
|
|
`- ${this.commands.length} commands`,
|
|
];
|
|
|
|
this.logger.log(chalk.blue(message.join('\n ')));
|
|
}
|
|
|
|
override async runMigrationCommand(
|
|
passedParams: string[],
|
|
options: ActiveOrSuspendedWorkspacesMigrationCommandOptions,
|
|
): Promise<void> {
|
|
try {
|
|
this.setUpgradeContextVersionsAndCommandsForCurrentAppVersion();
|
|
|
|
const shouldSkipUpgradeIfFreshInstallation =
|
|
await this.shouldSkipUpgradeIfFreshInstallation();
|
|
|
|
if (shouldSkipUpgradeIfFreshInstallation) {
|
|
this.logger.log(
|
|
chalk.blue('Fresh installation detected, skipping migration'),
|
|
);
|
|
|
|
return;
|
|
}
|
|
|
|
const workspacesThatAreBelowFromWorkspaceVersion =
|
|
await this.workspacesThatAreBelowFromWorkspaceVersion(
|
|
this.fromWorkspaceVersion,
|
|
);
|
|
|
|
if (workspacesThatAreBelowFromWorkspaceVersion.length > 0) {
|
|
this.migrationReport.fail.push(
|
|
...workspacesThatAreBelowFromWorkspaceVersion.map((workspace) => ({
|
|
error: new Error(
|
|
`Unable to run the upgrade command. Aborting the upgrade process.
|
|
Please ensure that all workspaces are on at least the previous minor version (${this.fromWorkspaceVersion.version}).
|
|
If any workspaces are not on the previous minor version, roll back to that version and run the upgrade command again.`,
|
|
),
|
|
workspaceId: workspace.id,
|
|
})),
|
|
);
|
|
}
|
|
} catch (error) {
|
|
this.migrationReport.fail.push({
|
|
error,
|
|
workspaceId: 'global',
|
|
});
|
|
}
|
|
|
|
if (this.migrationReport.fail.length > 0) {
|
|
this.migrationReport.fail.forEach(({ error, workspaceId }) =>
|
|
this.logger.error(
|
|
`Error in workspace ${workspaceId}: ${error.message}`,
|
|
),
|
|
);
|
|
|
|
return;
|
|
}
|
|
|
|
await this.runCoreMigrations();
|
|
await super.runMigrationCommand(passedParams, options);
|
|
}
|
|
|
|
override async runOnWorkspace(args: RunOnWorkspaceArgs): Promise<void> {
|
|
this.setUpgradeContextVersionsAndCommandsForCurrentAppVersion();
|
|
|
|
const { workspaceId, index, total, options } = args;
|
|
|
|
this.logger.log(
|
|
chalk.blue(
|
|
`${options.dryRun ? '(dry run) ' : ''}Upgrading workspace ${workspaceId} from=${this.fromWorkspaceVersion} to=${this.currentAppVersion} ${index + 1}/${total}`,
|
|
),
|
|
);
|
|
|
|
const workspaceVersionCompareResult =
|
|
await this.retrieveWorkspaceVersionAndCompareToWorkspaceFromVersion(
|
|
workspaceId,
|
|
);
|
|
|
|
switch (workspaceVersionCompareResult) {
|
|
case 'lower': {
|
|
throw new Error(
|
|
`WORKSPACE_VERSION_MISSMATCH Upgrade for workspace ${workspaceId} failed as its version is beneath fromWorkspaceVersion=${this.fromWorkspaceVersion.version}`,
|
|
);
|
|
}
|
|
case 'equal': {
|
|
for (const command of this.commands) {
|
|
await command.runOnWorkspace(args);
|
|
}
|
|
|
|
if (!options.dryRun) {
|
|
await this.workspaceRepository.update(
|
|
{ id: workspaceId },
|
|
{ version: this.currentAppVersion.version },
|
|
);
|
|
}
|
|
|
|
this.logger.log(
|
|
chalk.blue(`Upgrade for workspace ${workspaceId} completed.`),
|
|
);
|
|
|
|
return;
|
|
}
|
|
case 'higher': {
|
|
this.logger.log(
|
|
chalk.blue(
|
|
`Upgrade for workspace ${workspaceId} ignored as is already at a higher version.`,
|
|
),
|
|
);
|
|
|
|
return;
|
|
}
|
|
default: {
|
|
throw new Error(
|
|
`Should never occur, encountered unexpected value from retrieveWorkspaceVersionAndCompareToWorkspaceFromVersion ${workspaceVersionCompareResult}`,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
private retrieveCurrentAppVersion() {
|
|
const appVersion = this.twentyConfigService.get('APP_VERSION');
|
|
|
|
if (!isDefined(appVersion)) {
|
|
throw new Error(
|
|
'Cannot run upgrade command when APP_VERSION is not defined, please double check your env variables',
|
|
);
|
|
}
|
|
|
|
try {
|
|
return new SemVer(appVersion);
|
|
} catch {
|
|
throw new Error(
|
|
`Should never occur, APP_VERSION is invalid ${appVersion}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
private async retrieveWorkspaceVersionAndCompareToWorkspaceFromVersion(
|
|
workspaceId: string,
|
|
): Promise<CompareVersionMajorAndMinorReturnType> {
|
|
const workspace = await this.workspaceRepository.findOneByOrFail({
|
|
id: workspaceId,
|
|
});
|
|
const currentWorkspaceVersion = workspace.version;
|
|
|
|
if (!isDefined(currentWorkspaceVersion)) {
|
|
throw new Error(`WORKSPACE_VERSION_NOT_DEFINED workspace=${workspaceId}`);
|
|
}
|
|
|
|
return compareVersionMajorAndMinor(
|
|
currentWorkspaceVersion,
|
|
this.fromWorkspaceVersion.version,
|
|
);
|
|
}
|
|
}
|