From f802294c84fa7d912b2622d84ab3c00f527f9d9f Mon Sep 17 00:00:00 2001 From: Charles Bochet Date: Fri, 5 Sep 2025 15:58:17 +0200 Subject: [PATCH] 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 --- .../upgrade.command-runner.spec.ts.snap | 18 +- .../__tests__/upgrade.command-runner.spec.ts | 132 +++++---------- .../command-runners/upgrade.command-runner.ts | 154 +++++++++++++++++- .../commands/database-command.module.ts | 2 - ...flow-versions-and-workflow-runs.command.ts | 2 +- .../1-5/1-5-migrate-views-to-core.command.ts} | 7 +- ...5-remove-favorite-view-relation.command.ts | 4 +- .../1-5/1-5-upgrade-version-command.module.ts | 15 +- .../1-6/1-6-upgrade-version-command.module.ts | 25 +++ .../upgrade-version-command.module.ts | 9 +- .../upgrade.command.ts | 142 ++-------------- 11 files changed, 264 insertions(+), 246 deletions(-) rename packages/twenty-server/src/database/commands/{views-migration/migrate-views-to-core.command.ts => upgrade-version-command/1-5/1-5-migrate-views-to-core.command.ts} (98%) create mode 100644 packages/twenty-server/src/database/commands/upgrade-version-command/1-6/1-6-upgrade-version-command.module.ts diff --git a/packages/twenty-server/src/database/commands/command-runners/__tests__/__snapshots__/upgrade.command-runner.spec.ts.snap b/packages/twenty-server/src/database/commands/command-runners/__tests__/__snapshots__/upgrade.command-runner.spec.ts.snap index eeee9e07d1..c258a6e1b9 100644 --- a/packages/twenty-server/src/database/commands/command-runners/__tests__/__snapshots__/upgrade.command-runner.spec.ts.snap +++ b/packages/twenty-server/src/database/commands/command-runners/__tests__/__snapshots__/upgrade.command-runner.spec.ts.snap @@ -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.] +`; diff --git a/packages/twenty-server/src/database/commands/command-runners/__tests__/upgrade.command-runner.spec.ts b/packages/twenty-server/src/database/commands/command-runners/__tests__/upgrade.command-runner.spec.ts index e998e762de..85dd318b73 100644 --- a/packages/twenty-server/src/database/commands/command-runners/__tests__/upgrade.command-runner.spec.ts +++ b/packages/twenty-server/src/database/commands/command-runners/__tests__/upgrade.command-runner.spec.ts @@ -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; 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>( 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; + 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(); + }, + ); }); }); diff --git a/packages/twenty-server/src/database/commands/command-runners/upgrade.command-runner.ts b/packages/twenty-server/src/database/commands/command-runners/upgrade.command-runner.ts index 16c473aa2a..c7132a32d6 100644 --- a/packages/twenty-server/src/database/commands/command-runners/upgrade.command-runner.ts +++ b/packages/twenty-server/src/database/commands/command-runners/upgrade.command-runner.ts @@ -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; +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 { + const activeWorkspaceOrSuspendedWorkspaceCount = + await this.loadActiveOrSuspendedWorkspace(); + + return activeWorkspaceOrSuspendedWorkspaceCount.length === 0; + } + + async runCoreMigrations(): Promise { + 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[]> { + 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 { + 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 { this.setUpgradeContextVersionsAndCommandsForCurrentAppVersion(); diff --git a/packages/twenty-server/src/database/commands/database-command.module.ts b/packages/twenty-server/src/database/commands/database-command.module.ts index baaeb1311f..7c271d89ba 100644 --- a/packages/twenty-server/src/database/commands/database-command.module.ts +++ b/packages/twenty-server/src/database/commands/database-command.module.ts @@ -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, ], diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-5/1-5-add-positions-to-workflow-versions-and-workflow-runs.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-5/1-5-add-positions-to-workflow-versions-and-workflow-runs.command.ts index 0e7d6135d8..94fae41a32 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-5/1-5-add-positions-to-workflow-versions-and-workflow-runs.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-5/1-5-add-positions-to-workflow-versions-and-workflow-runs.command.ts @@ -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, diff --git a/packages/twenty-server/src/database/commands/views-migration/migrate-views-to-core.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-5/1-5-migrate-views-to-core.command.ts similarity index 98% rename from packages/twenty-server/src/database/commands/views-migration/migrate-views-to-core.command.ts rename to packages/twenty-server/src/database/commands/upgrade-version-command/1-5/1-5-migrate-views-to-core.command.ts index 0c7f4661f8..441abb9360 100644 --- a/packages/twenty-server/src/database/commands/views-migration/migrate-views-to-core.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-5/1-5-migrate-views-to-core.command.ts @@ -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, - private readonly featureFlagService: FeatureFlagService, protected readonly twentyORMGlobalManager: TwentyORMGlobalManager, @InjectDataSource() private readonly coreDataSource: DataSource, diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-5/1-5-remove-favorite-view-relation.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-5/1-5-remove-favorite-view-relation.command.ts index a2b85d154b..b9ac9468ec 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-5/1-5-remove-favorite-view-relation.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-5/1-5-remove-favorite-view-relation.command.ts @@ -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, diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-5/1-5-upgrade-version-command.module.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-5/1-5-upgrade-version-command.module.ts index dc521dac37..9041bc4159 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-5/1-5-upgrade-version-command.module.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-5/1-5-upgrade-version-command.module.ts @@ -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 {} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-6/1-6-upgrade-version-command.module.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-6/1-6-upgrade-version-command.module.ts new file mode 100644 index 0000000000..92fb5610a5 --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-6/1-6-upgrade-version-command.module.ts @@ -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 {} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade-version-command.module.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade-version-command.module.ts index 56a866fc97..c25aa8d154 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade-version-command.module.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade-version-command.module.ts @@ -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 {} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts index eb216e31ff..165d5438e1 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts @@ -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, - ) {} - - // 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 { - const activeWorkspaceOrSuspendedWorkspaceCount = - await this.loadActiveOrSuspendedWorkspace(); - - return activeWorkspaceOrSuspendedWorkspaceCount.length === 0; - } - - async runMigrations(): Promise { - 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 { - 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 { - 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); } }