Improve upgrade command and prepare 1.5 release (#14325)

In this PR:
- refactor the upgrade command / upgrade command runner to keep upgrade
command as light as possible (all wrapping logic should go to upgrade
command runner)
- prevent any upgrade if there is at least one workspace.version <
previsousVersion ==> this leads to corrupted state where only core
migrations are run if the self-hoster is skipping a version
This commit is contained in:
Charles Bochet
2025-09-05 15:58:17 +02:00
committed by GitHub
parent d5ef4cbbff
commit f802294c84
11 changed files with 264 additions and 246 deletions
@@ -8,12 +8,14 @@ exports[`UpgradeCommandRunner Workspace upgrade should fail when current version
exports[`UpgradeCommandRunner Workspace upgrade should fail when previous version is not found 1`] = `[Error: No previous version found for version 1.0.0. Please review the "allCommands" record. Available versions are: 1.0.0, 2.0.0]`;
exports[`UpgradeCommandRunner Workspace upgrade should fail when workspace version is not defined 1`] = `[Error: WORKSPACE_VERSION_NOT_DEFINED workspace=workspace_0]`;
exports[`UpgradeCommandRunner Workspace upgrade should fail when workspace version is not defined 1`] = `
[Error: Unable to run the upgrade command. Aborting the upgrade process.
Please ensure that all workspaces are on at least the previous minor version (1.0.0).
If any workspaces are not on the previous minor version, roll back to that version and run the upgrade command again.]
`;
exports[`UpgradeCommandRunner Workspace upgrade should fail when workspace version is not equal to fromVersion 1`] = `[Error: WORKSPACE_VERSION_MISSMATCH Upgrade for workspace workspace_0 failed as its version is beneath fromWorkspaceVersion=1.0.0]`;
exports[`UpgradeCommandRunner should run upgrade command with failing and successful workspaces 1`] = `[Error: WORKSPACE_VERSION_MISSMATCH Upgrade for workspace outated_version_workspace failed as its version is beneath fromWorkspaceVersion=1.0.0]`;
exports[`UpgradeCommandRunner should run upgrade command with failing and successful workspaces 2`] = `[Error: Received invalid version: invalid 1.0.0]`;
exports[`UpgradeCommandRunner should run upgrade command with failing and successful workspaces 3`] = `[Error: WORKSPACE_VERSION_NOT_DEFINED workspace=null_version_workspace]`;
exports[`UpgradeCommandRunner Workspace upgrade should fail when workspace version is not equal to fromVersion 1`] = `
[Error: Unable to run the upgrade command. Aborting the upgrade process.
Please ensure that all workspaces are on at least the previous minor version (1.0.0).
If any workspaces are not on the previous minor version, roll back to that version and run the upgrade command again.]
`;
@@ -1,7 +1,10 @@
import { Test, type TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { type EachTestingContext } from 'twenty-shared/testing';
import {
eachTestingContextFilter,
type EachTestingContext,
} from 'twenty-shared/testing';
import { type Repository } from 'typeorm';
import { UpgradeCommandRunner } from 'src/database/commands/command-runners/upgrade.command-runner';
@@ -125,6 +128,7 @@ describe('UpgradeCommandRunner', () => {
let syncWorkspaceMetadataCommand: jest.Mocked<SyncWorkspaceMetadataCommand>;
let runAfterSyncMetadataSpy: jest.SpyInstance;
let runBeforeSyncMetadataSpy: jest.SpyInstance;
let runCoreMigrationsSpy: jest.SpyInstance;
let twentyORMGlobalManagerSpy: TwentyORMGlobalManager;
type BuildModuleAndSetupSpiesArgs = {
@@ -165,6 +169,9 @@ describe('UpgradeCommandRunner', () => {
'runAfterSyncMetadata',
);
jest.spyOn(upgradeCommandRunner, 'runOnWorkspace');
runCoreMigrationsSpy = jest
.spyOn(upgradeCommandRunner, 'runCoreMigrations')
.mockImplementation(() => Promise.resolve());
workspaceRepository = module.get<Repository<Workspace>>(
getRepositoryToken(Workspace),
@@ -213,76 +220,6 @@ describe('UpgradeCommandRunner', () => {
].forEach((fn) => expect(fn).not.toHaveBeenCalled());
});
it('should run upgrade command with failing and successful workspaces', async () => {
const outdatedVersionWorkspaces = generateMockWorkspace({
id: 'outated_version_workspace',
version: '0.42.42',
});
const invalidVersionWorkspace = generateMockWorkspace({
id: 'invalid_version_workspace',
version: 'invalid',
});
const nullVersionWorkspace = generateMockWorkspace({
id: 'null_version_workspace',
version: null,
});
const numberOfValidWorkspace = 4;
const failingWorkspaces = [
outdatedVersionWorkspaces,
invalidVersionWorkspace,
nullVersionWorkspace,
];
const totalWorkspace = numberOfValidWorkspace + failingWorkspaces.length;
const appVersion = 'v2.0.0';
const expectedToVersion = '2.0.0';
await buildModuleAndSetupSpies({
numberOfWorkspace: numberOfValidWorkspace,
workspaces: failingWorkspaces,
appVersion,
});
// @ts-expect-error legacy noImplicitAny
const passedParams = [];
const options = {};
// @ts-expect-error legacy noImplicitAny
await upgradeCommandRunner.run(passedParams, options);
// Common assertions
const { fail: failReport, success: successReport } =
upgradeCommandRunner.migrationReport;
[
twentyORMGlobalManagerSpy.destroyDataSourceForWorkspace,
upgradeCommandRunner.runOnWorkspace,
].forEach((fn) => expect(fn).toHaveBeenCalledTimes(totalWorkspace));
expect(failReport.length + successReport.length).toBe(totalWorkspace);
// Success assertions
[
upgradeCommandRunner.runBeforeSyncMetadata,
syncWorkspaceMetadataCommand.runOnWorkspace,
upgradeCommandRunner.runAfterSyncMetadata,
].forEach((fn) => expect(fn).toHaveBeenCalledTimes(numberOfValidWorkspace));
expect(successReport.length).toBe(numberOfValidWorkspace);
expect(workspaceRepository.update).toHaveBeenNthCalledWith(
numberOfValidWorkspace,
{ id: expect.any(String) },
{ version: expectedToVersion },
);
// Failing assertions
expect(failReport.length).toBe(failingWorkspaces.length);
failReport.forEach((report) => {
expect(
failingWorkspaces.some(
(workspace) => workspace.id === report.workspaceId,
),
).toBe(true);
expect(report.error).toMatchSnapshot();
});
});
it('should run upgrade over several workspaces', async () => {
const numberOfWorkspace = 42;
const appVersion = '2.0.0';
@@ -383,7 +320,7 @@ describe('UpgradeCommandRunner', () => {
},
];
it.each(successfulTestUseCases)(
it.each(eachTestingContextFilter(successfulTestUseCases))(
'$title',
async ({ context: { input } }) => {
await buildModuleAndSetupSpies(input);
@@ -400,8 +337,9 @@ describe('UpgradeCommandRunner', () => {
expect(failReport.length).toBe(0);
expect(successReport.length).toBe(1);
expect(runAfterSyncMetadataSpy).toBeCalledTimes(1);
expect(runBeforeSyncMetadataSpy).toBeCalledTimes(1);
expect(runCoreMigrationsSpy).toHaveBeenCalledTimes(1);
expect(runAfterSyncMetadataSpy).toHaveBeenCalledTimes(1);
expect(runBeforeSyncMetadataSpy).toHaveBeenCalledTimes(1);
const { workspaceId } = successReport[0];
expect(workspaceId).toBe('workspace_0');
@@ -412,6 +350,9 @@ describe('UpgradeCommandRunner', () => {
describe('Workspace upgrade should fail', () => {
const failingTestUseCases: EachTestingContext<{
input: Omit<BuildModuleAndSetupSpiesArgs, 'numberOfWorkspace'>;
output?: {
failReportWorkspaceId: string;
};
}>[] = [
{
title: 'when workspace version is not equal to fromVersion',
@@ -422,6 +363,9 @@ describe('UpgradeCommandRunner', () => {
version: '0.1.0',
},
},
output: {
failReportWorkspaceId: 'workspace_0',
},
},
},
{
@@ -432,6 +376,9 @@ describe('UpgradeCommandRunner', () => {
version: null,
},
},
output: {
failReportWorkspaceId: 'workspace_0',
},
},
},
{
@@ -440,6 +387,9 @@ describe('UpgradeCommandRunner', () => {
input: {
appVersion: null,
},
output: {
failReportWorkspaceId: 'global',
},
},
},
{
@@ -448,6 +398,9 @@ describe('UpgradeCommandRunner', () => {
input: {
appVersion: '42.0.0',
},
output: {
failReportWorkspaceId: 'global',
},
},
},
{
@@ -468,25 +421,26 @@ describe('UpgradeCommandRunner', () => {
},
];
it.each(failingTestUseCases)('$title', async ({ context: { input } }) => {
await buildModuleAndSetupSpies(input);
it.each(eachTestingContextFilter(failingTestUseCases))(
'$title',
async ({ context: { input, output } }) => {
await buildModuleAndSetupSpies(input);
// @ts-expect-error legacy noImplicitAny
const passedParams = [];
const options = {};
const passedParams: string[] = [];
const options = {};
// @ts-expect-error legacy noImplicitAny
await upgradeCommandRunner.run(passedParams, options);
await upgradeCommandRunner.run(passedParams, options);
const { fail: failReport, success: successReport } =
upgradeCommandRunner.migrationReport;
const { fail: failReport, success: successReport } =
upgradeCommandRunner.migrationReport;
expect(successReport.length).toBe(0);
expect(failReport.length).toBe(1);
const { workspaceId, error } = failReport[0];
expect(successReport.length).toBe(0);
expect(failReport.length).toBe(1);
const { workspaceId, error } = failReport[0];
expect(workspaceId).toBe('workspace_0');
expect(error).toMatchSnapshot();
});
expect(workspaceId).toBe(output?.failReportWorkspaceId ?? 'global');
expect(error).toMatchSnapshot();
},
);
});
});
@@ -1,11 +1,16 @@
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 { Repository } from 'typeorm';
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
import { In, Repository } from 'typeorm';
import {
ActiveOrSuspendedWorkspacesMigrationCommandOptions,
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
type RunOnWorkspaceArgs,
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
@@ -24,6 +29,8 @@ export type VersionCommands = {
afterSyncMetadata: ActiveOrSuspendedWorkspacesMigrationCommandRunner[];
};
export type AllCommands = Record<string, VersionCommands>;
const execPromise = promisify(exec);
export abstract class UpgradeCommandRunner extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
private fromWorkspaceVersion: SemVer;
private currentAppVersion: SemVer;
@@ -41,14 +48,99 @@ export abstract class UpgradeCommandRunner extends ActiveOrSuspendedWorkspacesMi
super(workspaceRepository, twentyORMGlobalManager);
}
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/src/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<Workspace, '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 ugpradeContextIsAlreadyDefined = [
const upgradeContextIsAlreadyDefined = [
this.currentAppVersion,
this.commands,
this.fromWorkspaceVersion,
].every(isDefined);
if (ugpradeContextIsAlreadyDefined) {
if (upgradeContextIsAlreadyDefined) {
return;
}
@@ -87,6 +179,62 @@ export abstract class UpgradeCommandRunner extends ActiveOrSuspendedWorkspacesMi
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();
@@ -4,7 +4,6 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { CronRegisterAllCommand } from 'src/database/commands/cron-register-all.command';
import { ConfirmationQuestion } from 'src/database/commands/questions/confirmation.question';
import { UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/upgrade-version-command.module';
import { MigrateViewsToCoreCommand } from 'src/database/commands/views-migration/migrate-views-to-core.command';
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
import { ApiKeyModule } from 'src/engine/core-modules/api-key/api-key.module';
import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module';
@@ -50,7 +49,6 @@ import { DataSeedWorkspaceCommand } from './data-seed-dev-workspace.command';
],
providers: [
DataSeedWorkspaceCommand,
MigrateViewsToCoreCommand,
ConfirmationQuestion,
CronRegisterAllCommand,
],
@@ -41,7 +41,7 @@ export type AddPositionsToWorkflowVersionsAndWorkflowRunsOptions =
name: 'upgrade:1-5:add-positions-to-workflow-versions-and-workflow-runs',
description: 'Add positions to workflow versions and workflow runs',
})
export class AddPositionsToWorkflowVersionsAndWorkflowRuns extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
export class AddPositionsToWorkflowVersionsAndWorkflowRunsCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
constructor(
@InjectRepository(Workspace)
protected readonly workspaceRepository: Repository<Workspace>,
@@ -8,7 +8,6 @@ import {
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
type RunOnWorkspaceArgs,
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { ViewFieldEntity } from 'src/engine/core-modules/view/entities/view-field.entity';
import { ViewFilterGroupEntity } from 'src/engine/core-modules/view/entities/view-filter-group.entity';
import { ViewFilterEntity } from 'src/engine/core-modules/view/entities/view-filter.entity';
@@ -34,15 +33,13 @@ import { convertViewFilterOperandToCoreOperand } from 'src/modules/view/utils/co
import { convertViewFilterWorkspaceValueToCoreValue } from 'src/modules/view/utils/convert-view-filter-workspace-value-to-core-value';
@Command({
name: 'migrate:views-to-core',
description:
'Migrate views from workspace schemas to core schema and enable IS_CORE_VIEW_SYNCING_ENABLED feature flag',
name: 'upgrade:1-5:migrate-views-to-core',
description: 'Migrate views from workspace schemas to core schema',
})
export class MigrateViewsToCoreCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
constructor(
@InjectRepository(Workspace)
protected readonly workspaceRepository: Repository<Workspace>,
private readonly featureFlagService: FeatureFlagService,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
@InjectDataSource()
private readonly coreDataSource: DataSource,
@@ -22,10 +22,10 @@ import {
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
@Command({
name: 'migrate:1-5:remove-favorite-view-relation',
name: 'upgrade:1-5:remove-favorite-view-relation',
description: 'Remove favorite view relation.',
})
export class RemoveFavoriteViewRelation extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
export class RemoveFavoriteViewRelationCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
constructor(
@InjectRepository(Workspace)
protected readonly workspaceRepository: Repository<Workspace>,
@@ -1,8 +1,9 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AddPositionsToWorkflowVersionsAndWorkflowRuns } from 'src/database/commands/upgrade-version-command/1-5/1-5-add-positions-to-workflow-versions-and-workflow-runs.command';
import { RemoveFavoriteViewRelation } from 'src/database/commands/upgrade-version-command/1-5/1-5-remove-favorite-view-relation.command';
import { AddPositionsToWorkflowVersionsAndWorkflowRunsCommand } from 'src/database/commands/upgrade-version-command/1-5/1-5-add-positions-to-workflow-versions-and-workflow-runs.command';
import { MigrateViewsToCoreCommand } from 'src/database/commands/upgrade-version-command/1-5/1-5-migrate-views-to-core.command';
import { RemoveFavoriteViewRelationCommand } from 'src/database/commands/upgrade-version-command/1-5/1-5-remove-favorite-view-relation.command';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
@@ -22,12 +23,14 @@ import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/works
WorkspaceMetadataVersionModule,
],
providers: [
RemoveFavoriteViewRelation,
AddPositionsToWorkflowVersionsAndWorkflowRuns,
RemoveFavoriteViewRelationCommand,
AddPositionsToWorkflowVersionsAndWorkflowRunsCommand,
MigrateViewsToCoreCommand,
],
exports: [
RemoveFavoriteViewRelation,
AddPositionsToWorkflowVersionsAndWorkflowRuns,
RemoveFavoriteViewRelationCommand,
AddPositionsToWorkflowVersionsAndWorkflowRunsCommand,
MigrateViewsToCoreCommand,
],
})
export class V1_5_UpgradeVersionCommandModule {}
@@ -0,0 +1,25 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module';
import { WorkspaceSchemaManagerModule } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.module';
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
@Module({
imports: [
TypeOrmModule.forFeature([
Workspace,
FieldMetadataEntity,
ObjectMetadataEntity,
]),
WorkspaceDataSourceModule,
WorkspaceSchemaManagerModule,
WorkspaceMetadataVersionModule,
],
providers: [],
exports: [],
})
export class V1_6_UpgradeVersionCommandModule {}
@@ -7,10 +7,8 @@ import { V1_1_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-
import { V1_2_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-2/1-2-upgrade-version-command.module';
import { V1_3_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-3/1-3-upgrade-version-command.module';
import { V1_5_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-5/1-5-upgrade-version-command.module';
import {
DatabaseMigrationService,
UpgradeCommand,
} from 'src/database/commands/upgrade-version-command/upgrade.command';
import { V1_6_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-6/1-6-upgrade-version-command.module';
import { UpgradeCommand } from 'src/database/commands/upgrade-version-command/upgrade.command';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.module';
@@ -23,8 +21,9 @@ import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/worksp
V1_2_UpgradeVersionCommandModule,
V1_3_UpgradeVersionCommandModule,
V1_5_UpgradeVersionCommandModule,
V1_6_UpgradeVersionCommandModule,
WorkspaceSyncMetadataModule,
],
providers: [DatabaseMigrationService, UpgradeCommand],
providers: [UpgradeCommand],
})
export class UpgradeVersionCommandModule {}
@@ -1,13 +1,7 @@
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { exec } from 'child_process';
import { promisify } from 'util';
import chalk from 'chalk';
import { Command } from 'nest-commander';
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
import { In, Repository } from 'typeorm';
import { Repository } from 'typeorm';
import { type ActiveOrSuspendedWorkspacesMigrationCommandOptions } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import {
@@ -31,96 +25,13 @@ import { RemoveWorkflowRunsWithoutState } from 'src/database/commands/upgrade-ve
import { AddNextStepIdsToWorkflowRunsTrigger } from 'src/database/commands/upgrade-version-command/1-3/1-3-add-next-step-ids-to-workflow-runs-trigger.command';
import { AssignRolesToExistingApiKeysCommand } from 'src/database/commands/upgrade-version-command/1-3/1-3-assign-roles-to-existing-api-keys.command';
import { UpdateTimestampColumnTypeInWorkspaceSchemaCommand } from 'src/database/commands/upgrade-version-command/1-3/1-3-update-timestamp-column-type-in-workspace-schema.command';
import { AddPositionsToWorkflowVersionsAndWorkflowRuns } from 'src/database/commands/upgrade-version-command/1-5/1-5-add-positions-to-workflow-versions-and-workflow-runs.command';
import { RemoveFavoriteViewRelation } from 'src/database/commands/upgrade-version-command/1-5/1-5-remove-favorite-view-relation.command';
import { AddPositionsToWorkflowVersionsAndWorkflowRunsCommand } from 'src/database/commands/upgrade-version-command/1-5/1-5-add-positions-to-workflow-versions-and-workflow-runs.command';
import { MigrateViewsToCoreCommand } from 'src/database/commands/upgrade-version-command/1-5/1-5-migrate-views-to-core.command';
import { RemoveFavoriteViewRelationCommand } from 'src/database/commands/upgrade-version-command/1-5/1-5-remove-favorite-view-relation.command';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { SyncWorkspaceMetadataCommand } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/sync-workspace-metadata.command';
import { compareVersionMajorAndMinor } from 'src/utils/version/compare-version-minor-and-major';
const execPromise = promisify(exec);
@Injectable()
export class DatabaseMigrationService {
private logger = new Logger(DatabaseMigrationService.name);
constructor(
@InjectRepository(Workspace)
private readonly workspaceRepository: Repository<Workspace>,
) {}
// TODO centralize with ActiveOrSuspendedRunner method
private async loadActiveOrSuspendedWorkspace() {
return await this.workspaceRepository.find({
select: ['id', 'version'],
where: {
activationStatus: In([
WorkspaceActivationStatus.ACTIVE,
WorkspaceActivationStatus.SUSPENDED,
]),
},
order: {
id: 'ASC',
},
});
}
async shouldSkipUpgradeIfFreshInstallation(): Promise<boolean> {
const activeWorkspaceOrSuspendedWorkspaceCount =
await this.loadActiveOrSuspendedWorkspace();
return activeWorkspaceOrSuspendedWorkspaceCount.length === 0;
}
async runMigrations(): 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/src/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;
}
}
public async areAllWorkspacesAboveVersion0_53(): Promise<boolean> {
try {
const allActiveOrSuspendedWorkspaces =
await this.loadActiveOrSuspendedWorkspace();
if (allActiveOrSuspendedWorkspaces.length === 0) {
this.logger.log(
'No workspaces found. Running migrations for fresh installation.',
);
return true;
}
const workspacesBelowVersion = allActiveOrSuspendedWorkspaces.filter(
({ version }) =>
version === null ||
compareVersionMajorAndMinor(version, '0.53.0') === 'lower',
);
this.logger.log(
`Found ${workspacesBelowVersion.length} active or suspended workspaces that are below version 0.53.0 \n${workspacesBelowVersion.map((el) => el.id).join('\n')}`,
);
return workspacesBelowVersion.length === 0;
} catch (error) {
this.logger.error('Error checking workspaces below version:', error);
throw error;
}
}
}
@Command({
name: 'upgrade',
@@ -136,8 +47,6 @@ export class UpgradeCommand extends UpgradeCommandRunner {
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
protected readonly syncWorkspaceMetadataCommand: SyncWorkspaceMetadataCommand,
private readonly databaseMigrationService: DatabaseMigrationService,
// 0.54 Commands
protected readonly fixStandardSelectFieldsPositionCommand: FixStandardSelectFieldsPositionCommand,
protected readonly fixCreatedByDefaultValueCommand: FixCreatedByDefaultValueCommand,
@@ -165,8 +74,9 @@ export class UpgradeCommand extends UpgradeCommandRunner {
protected readonly updateTimestampColumnTypeInWorkspaceSchemaCommand: UpdateTimestampColumnTypeInWorkspaceSchemaCommand,
// 1.5 Commands
protected readonly removeFavoriteViewRelation: RemoveFavoriteViewRelation,
protected readonly addPositionsToWorkflowVersionsAndWorkflowRuns: AddPositionsToWorkflowVersionsAndWorkflowRuns,
protected readonly removeFavoriteViewRelationCommand: RemoveFavoriteViewRelationCommand,
protected readonly addPositionsToWorkflowVersionsAndWorkflowRunsCommand: AddPositionsToWorkflowVersionsAndWorkflowRunsCommand,
protected readonly migrateViewsToCoreCommand: MigrateViewsToCoreCommand,
) {
super(
workspaceRepository,
@@ -242,12 +152,18 @@ export class UpgradeCommand extends UpgradeCommandRunner {
const commands_150: VersionCommands = {
beforeSyncMetadata: [
this.removeFavoriteViewRelation,
this.addPositionsToWorkflowVersionsAndWorkflowRuns,
this.migrateViewsToCoreCommand,
this.removeFavoriteViewRelationCommand,
this.addPositionsToWorkflowVersionsAndWorkflowRunsCommand,
],
afterSyncMetadata: [],
};
const commands_160: VersionCommands = {
beforeSyncMetadata: [],
afterSyncMetadata: [],
};
this.allCommands = {
'0.53.0': commands_053,
'0.54.0': commands_054,
@@ -259,6 +175,7 @@ export class UpgradeCommand extends UpgradeCommandRunner {
'1.3.0': commands_130,
'1.4.0': commands_140,
'1.5.0': commands_150,
'1.6.0': commands_160,
};
}
@@ -266,31 +183,6 @@ export class UpgradeCommand extends UpgradeCommandRunner {
passedParams: string[],
options: ActiveOrSuspendedWorkspacesMigrationCommandOptions,
): Promise<void> {
const shouldSkipUpgradeIfFreshInstallation =
await this.databaseMigrationService.shouldSkipUpgradeIfFreshInstallation();
if (shouldSkipUpgradeIfFreshInstallation) {
this.logger.log(
chalk.blue('Fresh installation detected, skipping migration'),
);
return;
}
const shouldPreventFromUpgradingIfWorkspaceIsBelowVersion0_53 =
!(await this.databaseMigrationService.areAllWorkspacesAboveVersion0_53());
if (shouldPreventFromUpgradingIfWorkspaceIsBelowVersion0_53) {
this.logger.log(
chalk.red(
'Not able to run migrate command, aborting the whole migrate-upgrade operation',
),
);
throw new Error('Could not run migration aborting');
}
await this.databaseMigrationService.runMigrations();
await super.runMigrationCommand(passedParams, options);
return await super.runMigrationCommand(passedParams, options);
}
}