From 23b46059878c64291ffaf79b781da00df1ce8918 Mon Sep 17 00:00:00 2001 From: Paul Rastoin <45004772+prastoin@users.noreply.github.com> Date: Fri, 14 Mar 2025 19:21:44 +0100 Subject: [PATCH] [REFACTOR] Workspace version only `x.y.z` (#10910) # Introduction We want the APP_VERSION to be able to contains pre-release options, in a nutshell to be semVer compatible. But we want to have workspace, at least for the moment, that only store `x.y.z` and not `vx.y.z` or `x.y.z-alpha` version in database Explaining this refactor Related https://github.com/twentyhq/twenty/pull/10907 --- .../upgrade.command-runner.spec.ts.snap | 4 +- .../__tests__/upgrade.command-runner.spec.ts | 5 +- .../command-runners/upgrade.command-runner.ts | 38 ++++--- .../workspace/services/workspace.service.ts | 5 +- .../extract-version-major-minor-patch.spec.ts | 107 ++++++++++++++++++ .../extract-version-major-minor-patch.ts | 11 ++ 6 files changed, 149 insertions(+), 21 deletions(-) create mode 100644 packages/twenty-server/src/utils/version/__tests__/extract-version-major-minor-patch.spec.ts create mode 100644 packages/twenty-server/src/utils/version/extract-version-major-minor-patch.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 652d7bbe3..a97275391 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 @@ -2,7 +2,7 @@ exports[`UpgradeCommandRunner Workspace upgrade should fail when APP_VERSION is not defined 1`] = `[Error: Cannot run upgrade command when APP_VERSION is not defined, please double check your env variables]`; -exports[`UpgradeCommandRunner Workspace upgrade should fail when workspace version is not defined 1`] = `[Error: WORKSPACE_VERSION_NOT_DEFINED to=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 equal to fromVersion 1`] = `[Error: WORKSPACE_VERSION_MISSMATCH Upgrade for workspace workspace_0 failed as its version is beneath fromWorkspaceVersion=2.0.0]`; @@ -10,4 +10,4 @@ exports[`UpgradeCommandRunner should run upgrade command with failing and succes 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 to=2.0.0]`; +exports[`UpgradeCommandRunner should run upgrade command with failing and successful workspaces 3`] = `[Error: WORKSPACE_VERSION_NOT_DEFINED workspace=null_version_workspace]`; 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 73f4d53ef..dfc0843c4 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 @@ -248,7 +248,8 @@ describe('UpgradeCommandRunner', () => { nullVersionWorkspace, ]; const totalWorkspace = numberOfValidWorkspace + failingWorkspaces.length; - const appVersion = '2.0.0'; + const appVersion = 'v2.0.0'; + const expectedToVersion = '2.0.0'; await buildModuleAndSetupSpies({ numberOfWorkspace: numberOfValidWorkspace, @@ -280,7 +281,7 @@ describe('UpgradeCommandRunner', () => { expect(workspaceRepository.update).toHaveBeenNthCalledWith( numberOfValidWorkspace, { id: expect.any(String) }, - { version: appVersion }, + { version: expectedToVersion }, ); // Failing assertions 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 4157d673b..a361235fd 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 @@ -17,11 +17,7 @@ import { CompareVersionMajorAndMinorReturnType, compareVersionMajorAndMinor, } from 'src/utils/version/compare-version-minor-and-major'; - -type ValidateWorkspaceVersionEqualsWorkspaceFromVersionOrThrowArgs = { - workspaceId: string; - appVersion: string | undefined; -}; +import { extractVersionMajorMinorPatch } from 'src/utils/version/extract-version-major-minor-patch'; export abstract class UpgradeCommandRunner extends ActiveOrSuspendedWorkspacesMigrationCommandRunner { abstract readonly fromWorkspaceVersion: SemVer; @@ -39,19 +35,18 @@ export abstract class UpgradeCommandRunner extends ActiveOrSuspendedWorkspacesMi override async runOnWorkspace(args: RunOnWorkspaceArgs): Promise { const { workspaceId, index, total, options } = args; - const appVersion = this.environmentService.get('APP_VERSION'); this.logger.log( chalk.blue( `${options.dryRun ? '(dry run)' : ''} Upgrading workspace ${workspaceId} ${index + 1}/${total}`, ), ); + const toVersion = this.retrieveToVersionFromAppVersion(); const workspaceVersionCompareResult = - await this.retrieveWorkspaceVersionAndCompareToWorkspaceFromVersion({ - appVersion, + await this.retrieveWorkspaceVersionAndCompareToWorkspaceFromVersion( workspaceId, - }); + ); switch (workspaceVersionCompareResult) { case 'lower': { @@ -66,7 +61,7 @@ export abstract class UpgradeCommandRunner extends ActiveOrSuspendedWorkspacesMi await this.workspaceRepository.update( { id: workspaceId }, - { version: appVersion }, + { version: toVersion }, ); this.logger.log( chalk.blue(`Upgrade for workspace ${workspaceId} completed.`), @@ -91,16 +86,29 @@ export abstract class UpgradeCommandRunner extends ActiveOrSuspendedWorkspacesMi } } - private async retrieveWorkspaceVersionAndCompareToWorkspaceFromVersion({ - appVersion, - workspaceId, - }: ValidateWorkspaceVersionEqualsWorkspaceFromVersionOrThrowArgs): Promise { + private retrieveToVersionFromAppVersion() { + const appVersion = this.environmentService.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', ); } + const parsedVersion = extractVersionMajorMinorPatch(appVersion); + + if (!isDefined(parsedVersion)) { + throw new Error( + `Should never occur, APP_VERSION is invalid ${parsedVersion}`, + ); + } + + return parsedVersion; + } + + private async retrieveWorkspaceVersionAndCompareToWorkspaceFromVersion( + workspaceId: string, + ): Promise { // TODO remove after first release has been done using workspace_version if (!isDefined(this.VALIDATE_WORKSPACE_VERSION_FEATURE_FLAG)) { this.logger.warn( @@ -116,7 +124,7 @@ export abstract class UpgradeCommandRunner extends ActiveOrSuspendedWorkspacesMi const currentWorkspaceVersion = workspace.version; if (!isDefined(currentWorkspaceVersion)) { - throw new Error(`WORKSPACE_VERSION_NOT_DEFINED to=${appVersion}`); + throw new Error(`WORKSPACE_VERSION_NOT_DEFINED workspace=${workspaceId}`); } return compareVersionMajorAndMinor( diff --git a/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.ts b/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.ts index 66cf89f01..153f04d89 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.ts @@ -43,6 +43,7 @@ import { PermissionsService } from 'src/engine/metadata-modules/permissions/perm import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; import { WorkspaceManagerService } from 'src/engine/workspace-manager/workspace-manager.service'; import { DEFAULT_FEATURE_FLAGS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/default-feature-flags'; +import { extractVersionMajorMinorPatch } from 'src/utils/version/extract-version-major-minor-patch'; @Injectable() // eslint-disable-next-line @nx/workspace-inject-workspace-repository @@ -271,12 +272,12 @@ export class WorkspaceService extends TypeOrmQueryService { }); await this.userWorkspaceService.createWorkspaceMember(workspace.id, user); - const appVersion = this.environmentService.get('APP_VERSION') ?? null; + const appVersion = this.environmentService.get('APP_VERSION'); await this.workspaceRepository.update(workspace.id, { displayName: data.displayName, activationStatus: WorkspaceActivationStatus.ACTIVE, - version: appVersion, + version: extractVersionMajorMinorPatch(appVersion), }); return await this.workspaceRepository.findOneBy({ diff --git a/packages/twenty-server/src/utils/version/__tests__/extract-version-major-minor-patch.spec.ts b/packages/twenty-server/src/utils/version/__tests__/extract-version-major-minor-patch.spec.ts new file mode 100644 index 000000000..c5f05a6ad --- /dev/null +++ b/packages/twenty-server/src/utils/version/__tests__/extract-version-major-minor-patch.spec.ts @@ -0,0 +1,107 @@ +import { EachTestingContext } from 'twenty-shared'; + +import { extractVersionMajorMinorPatch } from 'src/utils/version/extract-version-major-minor-patch'; + +type IsSameVersionTestCase = EachTestingContext<{ + version: string | undefined; + expected: string | null; +}>; +describe('extract-version-major-minor-patch', () => { + const testCase: IsSameVersionTestCase[] = [ + { + context: { + version: '1.0.0', + expected: '1.0.0', + }, + title: 'Basic version', + }, + { + context: { + version: '2.3.4', + expected: '2.3.4', + }, + title: 'Version with non-zero patch', + }, + { + context: { + version: '0.1.0', + expected: '0.1.0', + }, + title: 'Version with zero major', + }, + { + context: { + version: '1.0.0-alpha', + expected: '1.0.0', + }, + title: 'Version with pre-release tag', + }, + { + context: { + version: '1.0.0-beta.1', + expected: '1.0.0', + }, + title: 'Version with pre-release tag and number', + }, + { + context: { + version: 'v1.0.0', + expected: '1.0.0', + }, + title: 'Version with v prefix', + }, + { + context: { + version: '42.42.42', + expected: '42.42.42', + }, + title: 'Version with large numbers', + }, + { + context: { + version: '1.2', + expected: null, + }, + title: 'Invalid version - missing patch number', + }, + { + context: { + version: 'invalid', + expected: null, + }, + title: 'Invalid version - not semver format', + }, + { + context: { + version: undefined, + expected: null, + }, + title: 'With undefined version', + }, + { + context: { + version: '1.0.0.0', + expected: null, + }, + title: 'Invalid version - too many segments', + }, + { + context: { + version: '1.a.0', + expected: null, + }, + title: 'Invalid version - non-numeric minor', + }, + { + context: { + version: '', + expected: null, + }, + title: 'Invalid version - empty string', + }, + ]; + + test.each(testCase)('$title', ({ context: { version, expected } }) => { + expect(extractVersionMajorMinorPatch(version)).toBe(expected); + }); +}); diff --git a/packages/twenty-server/src/utils/version/extract-version-major-minor-patch.ts b/packages/twenty-server/src/utils/version/extract-version-major-minor-patch.ts new file mode 100644 index 000000000..549bc1413 --- /dev/null +++ b/packages/twenty-server/src/utils/version/extract-version-major-minor-patch.ts @@ -0,0 +1,11 @@ +import semver from 'semver'; + +export const extractVersionMajorMinorPatch = (version: string | undefined) => { + const parsed = semver.parse(version); + + if (parsed === null) { + return null; + } + + return `${parsed.major}.${parsed.minor}.${parsed.patch}`; +};