[FEAT] New APP_VERSION env var inferred from tag & refactor upgrade-command to integrate versioning (#10751)
# Introduction This PR contains a big test file and few snapshots Related to https://github.com/twentyhq/core-team-issues/issues/487 ## New env var `APP_VERSION` Now will be injected directly in a built docker image the twenty's built version. Inferred from the build git tag name. Which mean on main or other `not a tag version` built APP_VERSION will be `null` ## New upgrade-commander-runner Refactored the upgrade command to be more strict regarding: - Version management - Sync metadata command always run - Added failing workspaces aggregator + logs on cleanup From now on the `upgrade` command will compare the `WORKSPACE_VERSION` to the `APP_VERSION` in order to bypass any workspace version != than the upgrade version `fromVersion` ## Existing commands Note that the version validation will be done only when passing by the `upgrade` command. Which means that running the following command `upgrade:x.y-some-specific-command` won't result in workspace version mutation This is to enforce that all an upgrade commands + sync-metadata has been run on a workspace ## Will do in other PR but related ### New workspace New workspace will now be inserted with version equal to the APP_VERSION they've been created by ### Old workspace Will create a command that should be ran outside of any `upgrade-runner` extending command, the command will have to be ran on every workspace before making the next release upgrade This command iterates over any active and suspended workspace that has `version` to `NULL` in order to update it `APP_VERSION` -1 minor ### SENTRY_RELEASE - Either deprecate SENTRY_RELEASE in favor of `APP_VERSION` => What about main with null version ? or create a new env var that would be `APP_COMMIT_SHA` instead of SENTRY third party ref ### Update CD to inject APP_VERSION from branch name ### Update docs and release logs Adding documentation for `APP_VERSION` ## Related PRs: https://github.com/twentyhq/twenty-infra/pull/181
This commit is contained in:
@ -66,6 +66,8 @@ ENV REACT_APP_SERVER_BASE_URL $REACT_APP_SERVER_BASE_URL
|
|||||||
ARG SENTRY_RELEASE
|
ARG SENTRY_RELEASE
|
||||||
ENV SENTRY_RELEASE $SENTRY_RELEASE
|
ENV SENTRY_RELEASE $SENTRY_RELEASE
|
||||||
|
|
||||||
|
ARG APP_VERSION
|
||||||
|
|
||||||
# Copy built applications from previous stages
|
# Copy built applications from previous stages
|
||||||
COPY --chown=1000 --from=twenty-server-build /app /app
|
COPY --chown=1000 --from=twenty-server-build /app /app
|
||||||
COPY --chown=1000 --from=twenty-server-build /app/packages/twenty-server /app/packages/twenty-server
|
COPY --chown=1000 --from=twenty-server-build /app/packages/twenty-server /app/packages/twenty-server
|
||||||
|
|||||||
@ -1,6 +1,11 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
|
# Set APP_VERSION only if it has a value
|
||||||
|
if [ ! -z "${APP_VERSION}" ]; then
|
||||||
|
export APP_VERSION="${APP_VERSION}"
|
||||||
|
fi
|
||||||
|
|
||||||
# Check if the initialization has already been done and that we enabled automatic migration
|
# Check if the initialization has already been done and that we enabled automatic migration
|
||||||
if [ "${DISABLE_DB_MIGRATIONS}" != "true" ] && [ ! -f /app/docker-data/db_status ]; then
|
if [ "${DISABLE_DB_MIGRATIONS}" != "true" ] && [ ! -f /app/docker-data/db_status ]; then
|
||||||
echo "Running database setup and migrations..."
|
echo "Running database setup and migrations..."
|
||||||
|
|||||||
@ -0,0 +1,13 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`UpgradeCommandRunner Workspace upgrade should fail when APP_VERSION is not defined 1`] = `[Error: Cannot run upgrade command when APP_VERSION is not defined]`;
|
||||||
|
|
||||||
|
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 equal to fromVersion 1`] = `[Error: WORKSPACE_VERSION_MISSMATCH Upgrade for workspace workspace_0 failed as its version is beneath fromWorkspaceVersion=2.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 to=2.0.0]`;
|
||||||
@ -0,0 +1,417 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||||
|
|
||||||
|
import { SemVer } from 'semver';
|
||||||
|
import { EachTestingContext } from 'twenty-shared';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
|
import { UpgradeCommandRunner } from 'src/database/commands/command-runners/upgrade.command-runner';
|
||||||
|
import { EnvironmentVariables } from 'src/engine/core-modules/environment/environment-variables';
|
||||||
|
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.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';
|
||||||
|
|
||||||
|
class TestUpgradeCommandRunnerV1 extends UpgradeCommandRunner {
|
||||||
|
fromWorkspaceVersion = new SemVer('1.0.0');
|
||||||
|
VALIDATE_WORKSPACE_VERSION_FEATURE_FLAG = true as const;
|
||||||
|
|
||||||
|
public override async runBeforeSyncMetadata(): Promise<void> {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async runAfterSyncMetadata(): Promise<void> {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class InvalidVersionUpgradeCommandRunner extends UpgradeCommandRunner {
|
||||||
|
fromWorkspaceVersion = new SemVer('invalid');
|
||||||
|
VALIDATE_WORKSPACE_VERSION_FEATURE_FLAG = true as const;
|
||||||
|
|
||||||
|
protected async runBeforeSyncMetadata(): Promise<void> {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async runAfterSyncMetadata(): Promise<void> {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TestUpgradeCommandRunnerV2 extends UpgradeCommandRunner {
|
||||||
|
fromWorkspaceVersion = new SemVer('2.0.0');
|
||||||
|
VALIDATE_WORKSPACE_VERSION_FEATURE_FLAG = true as const;
|
||||||
|
|
||||||
|
protected async runBeforeSyncMetadata(): Promise<void> {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async runAfterSyncMetadata(): Promise<void> {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type CommandRunnerValues =
|
||||||
|
| typeof TestUpgradeCommandRunnerV1
|
||||||
|
| typeof TestUpgradeCommandRunnerV2
|
||||||
|
| typeof InvalidVersionUpgradeCommandRunner;
|
||||||
|
|
||||||
|
const generateMockWorkspace = (overrides?: Partial<Workspace>) =>
|
||||||
|
({
|
||||||
|
id: 'workspace-id',
|
||||||
|
version: '1.0.0',
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
allowImpersonation: false,
|
||||||
|
isPublicInviteLinkEnabled: false,
|
||||||
|
displayName: 'Test Workspace',
|
||||||
|
domainName: 'test',
|
||||||
|
inviteHash: 'hash',
|
||||||
|
logo: null,
|
||||||
|
deletedAt: null,
|
||||||
|
activationStatus: 'active',
|
||||||
|
workspaceMembersCount: 1,
|
||||||
|
...overrides,
|
||||||
|
}) as Workspace;
|
||||||
|
|
||||||
|
type BuildUpgradeCommandModuleArgs = {
|
||||||
|
workspaces: Workspace[];
|
||||||
|
appVersion: string | null;
|
||||||
|
commandRunner: CommandRunnerValues;
|
||||||
|
};
|
||||||
|
const buildUpgradeCommandModule = async ({
|
||||||
|
workspaces,
|
||||||
|
appVersion,
|
||||||
|
commandRunner,
|
||||||
|
}: BuildUpgradeCommandModuleArgs) => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
commandRunner,
|
||||||
|
{
|
||||||
|
provide: getRepositoryToken(Workspace, 'core'),
|
||||||
|
useValue: {
|
||||||
|
findOneByOrFail: jest
|
||||||
|
.fn()
|
||||||
|
.mockImplementation((args) =>
|
||||||
|
workspaces.find((el) => el.id === args.id),
|
||||||
|
),
|
||||||
|
update: jest.fn(),
|
||||||
|
find: jest.fn().mockResolvedValue(workspaces),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: EnvironmentService,
|
||||||
|
useValue: {
|
||||||
|
get: jest
|
||||||
|
.fn()
|
||||||
|
.mockImplementation((key: keyof EnvironmentVariables) => {
|
||||||
|
switch (key) {
|
||||||
|
case 'APP_VERSION': {
|
||||||
|
return appVersion;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: TwentyORMGlobalManager,
|
||||||
|
useValue: {
|
||||||
|
connect: jest.fn(),
|
||||||
|
destroyDataSourceForWorkspace: jest.fn(),
|
||||||
|
getDataSourceForWorkspace: jest.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: SyncWorkspaceMetadataCommand,
|
||||||
|
useValue: {
|
||||||
|
runOnWorkspace: jest.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
return module;
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('UpgradeCommandRunner', () => {
|
||||||
|
let upgradeCommandRunner: TestUpgradeCommandRunnerV1;
|
||||||
|
let workspaceRepository: Repository<Workspace>;
|
||||||
|
let syncWorkspaceMetadataCommand: jest.Mocked<SyncWorkspaceMetadataCommand>;
|
||||||
|
let runAfterSyncMetadataSpy: jest.SpyInstance;
|
||||||
|
let runBeforeSyncMetadataSpy: jest.SpyInstance;
|
||||||
|
let twentyORMGlobalManagerSpy: TwentyORMGlobalManager;
|
||||||
|
|
||||||
|
type BuildModuleAndSetupSpiesArgs = {
|
||||||
|
numberOfWorkspace?: number;
|
||||||
|
workspaceOverride?: Partial<Workspace>;
|
||||||
|
workspaces?: Workspace[];
|
||||||
|
appVersion?: string | null;
|
||||||
|
commandRunner?: CommandRunnerValues;
|
||||||
|
};
|
||||||
|
const buildModuleAndSetupSpies = async ({
|
||||||
|
numberOfWorkspace = 1,
|
||||||
|
workspaceOverride,
|
||||||
|
workspaces,
|
||||||
|
commandRunner = TestUpgradeCommandRunnerV1,
|
||||||
|
appVersion = '2.0.0',
|
||||||
|
}: BuildModuleAndSetupSpiesArgs) => {
|
||||||
|
const generatedWorkspaces = Array.from(
|
||||||
|
{ length: numberOfWorkspace },
|
||||||
|
(_v, index) =>
|
||||||
|
generateMockWorkspace({
|
||||||
|
id: `workspace_${index}`,
|
||||||
|
...workspaceOverride,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const module = await buildUpgradeCommandModule({
|
||||||
|
commandRunner,
|
||||||
|
appVersion,
|
||||||
|
workspaces: [...generatedWorkspaces, ...(workspaces ?? [])],
|
||||||
|
});
|
||||||
|
|
||||||
|
upgradeCommandRunner = module.get(commandRunner);
|
||||||
|
runBeforeSyncMetadataSpy = jest.spyOn(
|
||||||
|
upgradeCommandRunner,
|
||||||
|
'runBeforeSyncMetadata',
|
||||||
|
);
|
||||||
|
runAfterSyncMetadataSpy = jest.spyOn(
|
||||||
|
upgradeCommandRunner,
|
||||||
|
'runAfterSyncMetadata',
|
||||||
|
);
|
||||||
|
jest.spyOn(upgradeCommandRunner, 'runOnWorkspace');
|
||||||
|
|
||||||
|
workspaceRepository = module.get<Repository<Workspace>>(
|
||||||
|
getRepositoryToken(Workspace, 'core'),
|
||||||
|
);
|
||||||
|
syncWorkspaceMetadataCommand = module.get(SyncWorkspaceMetadataCommand);
|
||||||
|
twentyORMGlobalManagerSpy = module.get<TwentyORMGlobalManager>(
|
||||||
|
TwentyORMGlobalManager,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should ignore and list as succesfull upgrade on workspace with higher version', async () => {
|
||||||
|
const higherVersionWorkspace = generateMockWorkspace({
|
||||||
|
id: 'higher_version_workspace',
|
||||||
|
version: '42.42.42',
|
||||||
|
});
|
||||||
|
const appVersion = '2.0.0';
|
||||||
|
|
||||||
|
await buildModuleAndSetupSpies({
|
||||||
|
numberOfWorkspace: 0,
|
||||||
|
workspaces: [higherVersionWorkspace],
|
||||||
|
appVersion,
|
||||||
|
});
|
||||||
|
const passedParams = [];
|
||||||
|
const options = {};
|
||||||
|
|
||||||
|
await upgradeCommandRunner.run(passedParams, options);
|
||||||
|
|
||||||
|
const { fail: failReport, success: successReport } =
|
||||||
|
upgradeCommandRunner.migrationReport;
|
||||||
|
|
||||||
|
expect(successReport.length).toBe(1);
|
||||||
|
expect(failReport.length).toBe(0);
|
||||||
|
|
||||||
|
[
|
||||||
|
twentyORMGlobalManagerSpy.destroyDataSourceForWorkspace,
|
||||||
|
upgradeCommandRunner.runOnWorkspace,
|
||||||
|
].forEach((fn) => expect(fn).toHaveBeenCalledTimes(1));
|
||||||
|
|
||||||
|
[
|
||||||
|
upgradeCommandRunner.runBeforeSyncMetadata,
|
||||||
|
syncWorkspaceMetadataCommand.runOnWorkspace,
|
||||||
|
upgradeCommandRunner.runAfterSyncMetadata,
|
||||||
|
workspaceRepository.update,
|
||||||
|
].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 = '2.0.0';
|
||||||
|
|
||||||
|
await buildModuleAndSetupSpies({
|
||||||
|
numberOfWorkspace: numberOfValidWorkspace,
|
||||||
|
workspaces: failingWorkspaces,
|
||||||
|
appVersion,
|
||||||
|
});
|
||||||
|
const passedParams = [];
|
||||||
|
const options = {};
|
||||||
|
|
||||||
|
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: appVersion },
|
||||||
|
);
|
||||||
|
|
||||||
|
// 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';
|
||||||
|
|
||||||
|
await buildModuleAndSetupSpies({
|
||||||
|
numberOfWorkspace,
|
||||||
|
appVersion,
|
||||||
|
});
|
||||||
|
const passedParams = [];
|
||||||
|
const options = {};
|
||||||
|
|
||||||
|
await upgradeCommandRunner.run(passedParams, options);
|
||||||
|
|
||||||
|
[
|
||||||
|
upgradeCommandRunner.runOnWorkspace,
|
||||||
|
upgradeCommandRunner.runBeforeSyncMetadata,
|
||||||
|
upgradeCommandRunner.runAfterSyncMetadata,
|
||||||
|
syncWorkspaceMetadataCommand.runOnWorkspace,
|
||||||
|
twentyORMGlobalManagerSpy.destroyDataSourceForWorkspace,
|
||||||
|
].forEach((fn) => expect(fn).toHaveBeenCalledTimes(numberOfWorkspace));
|
||||||
|
expect(workspaceRepository.update).toHaveBeenNthCalledWith(
|
||||||
|
numberOfWorkspace,
|
||||||
|
{ id: expect.any(String) },
|
||||||
|
{ version: appVersion },
|
||||||
|
);
|
||||||
|
expect(upgradeCommandRunner.migrationReport.success.length).toBe(42);
|
||||||
|
expect(upgradeCommandRunner.migrationReport.fail.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should run syncMetadataCommand betweensuccessful beforeSyncMetadataUpgradeCommandsToRun and afterSyncMetadataUpgradeCommandsToRun', async () => {
|
||||||
|
await buildModuleAndSetupSpies({});
|
||||||
|
const passedParams = [];
|
||||||
|
const options = {};
|
||||||
|
|
||||||
|
await upgradeCommandRunner.run(passedParams, options);
|
||||||
|
|
||||||
|
[
|
||||||
|
upgradeCommandRunner.runOnWorkspace,
|
||||||
|
upgradeCommandRunner.runBeforeSyncMetadata,
|
||||||
|
upgradeCommandRunner.runAfterSyncMetadata,
|
||||||
|
syncWorkspaceMetadataCommand.runOnWorkspace,
|
||||||
|
twentyORMGlobalManagerSpy.destroyDataSourceForWorkspace,
|
||||||
|
].forEach((fn) => expect(fn).toHaveBeenCalledTimes(1));
|
||||||
|
|
||||||
|
// Verify order of execution
|
||||||
|
const beforeSyncCall = runBeforeSyncMetadataSpy.mock.invocationCallOrder[0];
|
||||||
|
const afterSyncCall = runAfterSyncMetadataSpy.mock.invocationCallOrder[0];
|
||||||
|
const syncMetadataCall =
|
||||||
|
syncWorkspaceMetadataCommand.runOnWorkspace.mock.invocationCallOrder[0];
|
||||||
|
|
||||||
|
expect(beforeSyncCall).toBeLessThan(syncMetadataCall);
|
||||||
|
expect(syncMetadataCall).toBeLessThan(afterSyncCall);
|
||||||
|
expect(upgradeCommandRunner.migrationReport.success.length).toBe(1);
|
||||||
|
expect(upgradeCommandRunner.migrationReport.fail.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Workspace upgrade should fail', () => {
|
||||||
|
const failingTestUseCases: EachTestingContext<{
|
||||||
|
input: Omit<BuildModuleAndSetupSpiesArgs, 'numberOfWorkspace'>;
|
||||||
|
}>[] = [
|
||||||
|
{
|
||||||
|
title: 'when workspace version is not equal to fromVersion',
|
||||||
|
context: {
|
||||||
|
input: {
|
||||||
|
appVersion: '3.0.0',
|
||||||
|
commandRunner: TestUpgradeCommandRunnerV2,
|
||||||
|
workspaceOverride: {
|
||||||
|
version: '0.1.0',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'when workspace version is not defined',
|
||||||
|
context: {
|
||||||
|
input: {
|
||||||
|
workspaceOverride: {
|
||||||
|
version: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'when APP_VERSION is not defined',
|
||||||
|
context: {
|
||||||
|
input: {
|
||||||
|
appVersion: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
it.each(failingTestUseCases)('$title', async ({ context: { input } }) => {
|
||||||
|
await buildModuleAndSetupSpies(input);
|
||||||
|
|
||||||
|
const passedParams = [];
|
||||||
|
const options = {};
|
||||||
|
|
||||||
|
await upgradeCommandRunner.run(passedParams, options);
|
||||||
|
|
||||||
|
const { fail: failReport, success: successReport } =
|
||||||
|
upgradeCommandRunner.migrationReport;
|
||||||
|
|
||||||
|
expect(successReport.length).toBe(0);
|
||||||
|
expect(failReport.length).toBe(1);
|
||||||
|
const { workspaceId, error } = failReport[0];
|
||||||
|
|
||||||
|
expect(workspaceId).toBe('workspace_0');
|
||||||
|
expect(error).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw if upgrade command version is invalid', async () => {
|
||||||
|
await expect(
|
||||||
|
buildModuleAndSetupSpies({
|
||||||
|
commandRunner: InvalidVersionUpgradeCommandRunner,
|
||||||
|
}),
|
||||||
|
).rejects.toThrowErrorMatchingInlineSnapshot(`"Invalid Version: invalid"`);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -24,6 +24,16 @@ export type RunOnWorkspaceArgs = {
|
|||||||
total: number;
|
total: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type WorkspaceMigrationReport = {
|
||||||
|
fail: {
|
||||||
|
workspaceId: string;
|
||||||
|
error: Error;
|
||||||
|
}[];
|
||||||
|
success: {
|
||||||
|
workspaceId: string;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
|
||||||
export abstract class ActiveOrSuspendedWorkspacesMigrationCommandRunner<
|
export abstract class ActiveOrSuspendedWorkspacesMigrationCommandRunner<
|
||||||
Options extends
|
Options extends
|
||||||
ActiveOrSuspendedWorkspacesMigrationCommandOptions = ActiveOrSuspendedWorkspacesMigrationCommandOptions,
|
ActiveOrSuspendedWorkspacesMigrationCommandOptions = ActiveOrSuspendedWorkspacesMigrationCommandOptions,
|
||||||
@ -31,6 +41,10 @@ export abstract class ActiveOrSuspendedWorkspacesMigrationCommandRunner<
|
|||||||
private workspaceIds: string[] = [];
|
private workspaceIds: string[] = [];
|
||||||
private startFromWorkspaceId: string | undefined;
|
private startFromWorkspaceId: string | undefined;
|
||||||
private workspaceCountLimit: number | undefined;
|
private workspaceCountLimit: number | undefined;
|
||||||
|
public migrationReport: WorkspaceMigrationReport = {
|
||||||
|
fail: [],
|
||||||
|
success: [],
|
||||||
|
};
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected readonly workspaceRepository: Repository<Workspace>,
|
protected readonly workspaceRepository: Repository<Workspace>,
|
||||||
@ -107,7 +121,7 @@ export abstract class ActiveOrSuspendedWorkspacesMigrationCommandRunner<
|
|||||||
override async runMigrationCommand(
|
override async runMigrationCommand(
|
||||||
_passedParams: string[],
|
_passedParams: string[],
|
||||||
options: Options,
|
options: Options,
|
||||||
): Promise<void> {
|
) {
|
||||||
const activeWorkspaceIds =
|
const activeWorkspaceIds =
|
||||||
this.workspaceIds.length > 0
|
this.workspaceIds.length > 0
|
||||||
? this.workspaceIds
|
? this.workspaceIds
|
||||||
@ -117,39 +131,50 @@ export abstract class ActiveOrSuspendedWorkspacesMigrationCommandRunner<
|
|||||||
this.logger.log(chalk.yellow('Dry run mode: No changes will be applied'));
|
this.logger.log(chalk.yellow('Dry run mode: No changes will be applied'));
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
for (const [index, workspaceId] of activeWorkspaceIds.entries()) {
|
||||||
for (const [index, workspaceId] of activeWorkspaceIds.entries()) {
|
this.logger.log(
|
||||||
this.logger.log(
|
`Running command on workspace ${workspaceId} ${index + 1}/${activeWorkspaceIds.length}`,
|
||||||
`Running command on workspace ${workspaceId} ${index + 1}/${activeWorkspaceIds.length}`,
|
);
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const dataSource =
|
const dataSource =
|
||||||
await this.twentyORMGlobalManager.getDataSourceForWorkspace(
|
await this.twentyORMGlobalManager.getDataSourceForWorkspace(
|
||||||
workspaceId,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
|
|
||||||
await this.runOnWorkspace({
|
|
||||||
options,
|
|
||||||
workspaceId,
|
workspaceId,
|
||||||
dataSource,
|
false,
|
||||||
index: index,
|
|
||||||
total: activeWorkspaceIds.length,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.warn(
|
|
||||||
chalk.red(`Error in workspace ${workspaceId}: ${error.message}`),
|
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
|
await this.runOnWorkspace({
|
||||||
|
options,
|
||||||
|
workspaceId,
|
||||||
|
dataSource,
|
||||||
|
index: index,
|
||||||
|
total: activeWorkspaceIds.length,
|
||||||
|
});
|
||||||
|
this.migrationReport.success.push({
|
||||||
|
workspaceId,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
this.migrationReport.fail.push({
|
||||||
|
error,
|
||||||
|
workspaceId,
|
||||||
|
});
|
||||||
|
this.logger.warn(
|
||||||
|
chalk.red(`Error in workspace ${workspaceId}: ${error.message}`),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
await this.twentyORMGlobalManager.destroyDataSourceForWorkspace(
|
await this.twentyORMGlobalManager.destroyDataSourceForWorkspace(
|
||||||
workspaceId,
|
workspaceId,
|
||||||
);
|
);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(error);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
this.logger.error(error);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.migrationReport.fail.forEach(({ error, workspaceId }) =>
|
||||||
|
this.logger.error(`Error in workspace ${workspaceId}: ${error.message}`),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract runOnWorkspace(args: RunOnWorkspaceArgs): Promise<void>;
|
protected abstract runOnWorkspace(args: RunOnWorkspaceArgs): Promise<void>;
|
||||||
|
|||||||
@ -0,0 +1,132 @@
|
|||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
|
||||||
|
import chalk from 'chalk';
|
||||||
|
import { SemVer } from 'semver';
|
||||||
|
import { isDefined } from 'twenty-shared';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
|
||||||
|
RunOnWorkspaceArgs,
|
||||||
|
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||||
|
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.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';
|
||||||
|
|
||||||
|
type ValidateWorkspaceVersionEqualsWorkspaceFromVersionOrThrowArgs = {
|
||||||
|
workspaceId: string;
|
||||||
|
appVersion: string | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export abstract class UpgradeCommandRunner extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||||
|
abstract readonly fromWorkspaceVersion: SemVer;
|
||||||
|
public readonly VALIDATE_WORKSPACE_VERSION_FEATURE_FLAG?: true;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(Workspace, 'core')
|
||||||
|
protected readonly workspaceRepository: Repository<Workspace>,
|
||||||
|
protected readonly environmentService: EnvironmentService,
|
||||||
|
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||||
|
protected readonly syncWorkspaceMetadataCommand: SyncWorkspaceMetadataCommand,
|
||||||
|
) {
|
||||||
|
super(workspaceRepository, twentyORMGlobalManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
override async runOnWorkspace(args: RunOnWorkspaceArgs): Promise<void> {
|
||||||
|
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 workspaceVersionCompareResult =
|
||||||
|
await this.retrieveWorkspaceVersionAndCompareToWorkspaceFromVersion({
|
||||||
|
appVersion,
|
||||||
|
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': {
|
||||||
|
await this.runBeforeSyncMetadata(args);
|
||||||
|
await this.syncWorkspaceMetadataCommand.runOnWorkspace(args);
|
||||||
|
await this.runAfterSyncMetadata(args);
|
||||||
|
|
||||||
|
await this.workspaceRepository.update(
|
||||||
|
{ id: workspaceId },
|
||||||
|
{ version: appVersion },
|
||||||
|
);
|
||||||
|
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 async retrieveWorkspaceVersionAndCompareToWorkspaceFromVersion({
|
||||||
|
appVersion,
|
||||||
|
workspaceId,
|
||||||
|
}: ValidateWorkspaceVersionEqualsWorkspaceFromVersionOrThrowArgs) {
|
||||||
|
if (!isDefined(appVersion)) {
|
||||||
|
throw new Error(
|
||||||
|
'Cannot run upgrade command when APP_VERSION is not defined',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO remove after first release has been done using workspace_version
|
||||||
|
if (!isDefined(this.VALIDATE_WORKSPACE_VERSION_FEATURE_FLAG)) {
|
||||||
|
this.logger.warn(
|
||||||
|
'VALIDATE_WORKSPACE_VERSION_FEATURE_FLAG set to true ignoring workspace versions validation step',
|
||||||
|
);
|
||||||
|
const equalVersions = 0;
|
||||||
|
|
||||||
|
return equalVersions;
|
||||||
|
}
|
||||||
|
|
||||||
|
const workspace = await this.workspaceRepository.findOneByOrFail({
|
||||||
|
id: workspaceId,
|
||||||
|
});
|
||||||
|
const currentWorkspaceVersion = workspace.version;
|
||||||
|
|
||||||
|
if (!isDefined(currentWorkspaceVersion)) {
|
||||||
|
throw new Error(`WORKSPACE_VERSION_NOT_DEFINED to=${appVersion}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return compareVersionMajorAndMinor(
|
||||||
|
currentWorkspaceVersion,
|
||||||
|
this.fromWorkspaceVersion.version,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract runBeforeSyncMetadata(
|
||||||
|
args: RunOnWorkspaceArgs,
|
||||||
|
): Promise<void>;
|
||||||
|
protected abstract runAfterSyncMetadata(
|
||||||
|
args: RunOnWorkspaceArgs,
|
||||||
|
): Promise<void>;
|
||||||
|
}
|
||||||
@ -11,14 +11,12 @@ import { rawDataSource } from 'src/database/typeorm/raw/raw.datasource';
|
|||||||
import { InjectCacheStorage } from 'src/engine/core-modules/cache-storage/decorators/cache-storage.decorator';
|
import { InjectCacheStorage } from 'src/engine/core-modules/cache-storage/decorators/cache-storage.decorator';
|
||||||
import { CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service';
|
import { CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service';
|
||||||
import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum';
|
import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum';
|
||||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
|
||||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
import { WorkspaceManagerService } from 'src/engine/workspace-manager/workspace-manager.service';
|
import { WorkspaceManagerService } from 'src/engine/workspace-manager/workspace-manager.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class DataSeedDemoWorkspaceService {
|
export class DataSeedDemoWorkspaceService {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly environmentService: EnvironmentService,
|
|
||||||
private readonly workspaceManagerService: WorkspaceManagerService,
|
private readonly workspaceManagerService: WorkspaceManagerService,
|
||||||
@InjectRepository(Workspace, 'core')
|
@InjectRepository(Workspace, 'core')
|
||||||
protected readonly workspaceRepository: Repository<Workspace>,
|
protected readonly workspaceRepository: Repository<Workspace>,
|
||||||
|
|||||||
@ -13,7 +13,6 @@ import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
|||||||
import { FieldMetadataDefaultOption } from 'src/engine/metadata-modules/field-metadata/dtos/options.input';
|
import { FieldMetadataDefaultOption } from 'src/engine/metadata-modules/field-metadata/dtos/options.input';
|
||||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.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 { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||||
import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service';
|
|
||||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||||
import { tasksAssignedToMeView } from 'src/engine/workspace-manager/standard-objects-prefill-data/views/tasks-assigned-to-me';
|
import { tasksAssignedToMeView } from 'src/engine/workspace-manager/standard-objects-prefill-data/views/tasks-assigned-to-me';
|
||||||
import { TASK_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
|
import { TASK_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
|
||||||
@ -36,7 +35,6 @@ export class AddTasksAssignedToMeViewCommand extends ActiveOrSuspendedWorkspaces
|
|||||||
@InjectRepository(FieldMetadataEntity, 'metadata')
|
@InjectRepository(FieldMetadataEntity, 'metadata')
|
||||||
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
|
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
|
||||||
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||||
private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService,
|
|
||||||
) {
|
) {
|
||||||
super(workspaceRepository, twentyORMGlobalManager);
|
super(workspaceRepository, twentyORMGlobalManager);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,18 +1,17 @@
|
|||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
|
||||||
import chalk from 'chalk';
|
|
||||||
import { Command } from 'nest-commander';
|
import { Command } from 'nest-commander';
|
||||||
|
import { SemVer } from 'semver';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
import {
|
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||||
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
|
import { UpgradeCommandRunner } from 'src/database/commands/command-runners/upgrade.command-runner';
|
||||||
RunOnWorkspaceArgs,
|
|
||||||
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
|
||||||
import { AddTasksAssignedToMeViewCommand } from 'src/database/commands/upgrade-version-command/0-43/0-43-add-tasks-assigned-to-me-view.command';
|
import { AddTasksAssignedToMeViewCommand } from 'src/database/commands/upgrade-version-command/0-43/0-43-add-tasks-assigned-to-me-view.command';
|
||||||
import { MigrateIsSearchableForCustomObjectMetadataCommand } from 'src/database/commands/upgrade-version-command/0-43/0-43-migrate-is-searchable-for-custom-object-metadata.command';
|
import { MigrateIsSearchableForCustomObjectMetadataCommand } from 'src/database/commands/upgrade-version-command/0-43/0-43-migrate-is-searchable-for-custom-object-metadata.command';
|
||||||
import { MigrateRichTextContentPatchCommand } from 'src/database/commands/upgrade-version-command/0-43/0-43-migrate-rich-text-content-patch.command';
|
import { MigrateRichTextContentPatchCommand } from 'src/database/commands/upgrade-version-command/0-43/0-43-migrate-rich-text-content-patch.command';
|
||||||
import { MigrateSearchVectorOnNoteAndTaskEntitiesCommand } from 'src/database/commands/upgrade-version-command/0-43/0-43-migrate-search-vector-on-note-and-task-entities.command';
|
import { MigrateSearchVectorOnNoteAndTaskEntitiesCommand } from 'src/database/commands/upgrade-version-command/0-43/0-43-migrate-search-vector-on-note-and-task-entities.command';
|
||||||
import { UpdateDefaultViewRecordOpeningOnWorkflowObjectsCommand } from 'src/database/commands/upgrade-version-command/0-43/0-43-update-default-view-record-opening-on-workflow-objects.command';
|
import { UpdateDefaultViewRecordOpeningOnWorkflowObjectsCommand } from 'src/database/commands/upgrade-version-command/0-43/0-43-update-default-view-record-opening-on-workflow-objects.command';
|
||||||
|
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
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 { SyncWorkspaceMetadataCommand } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/sync-workspace-metadata.command';
|
||||||
@ -21,28 +20,31 @@ import { SyncWorkspaceMetadataCommand } from 'src/engine/workspace-manager/works
|
|||||||
name: 'upgrade',
|
name: 'upgrade',
|
||||||
description: 'Upgrade workspaces to the latest version',
|
description: 'Upgrade workspaces to the latest version',
|
||||||
})
|
})
|
||||||
export class UpgradeCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
export class UpgradeCommand extends UpgradeCommandRunner {
|
||||||
|
fromWorkspaceVersion = new SemVer('0.43.0');
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(Workspace, 'core')
|
@InjectRepository(Workspace, 'core')
|
||||||
protected readonly workspaceRepository: Repository<Workspace>,
|
protected readonly workspaceRepository: Repository<Workspace>,
|
||||||
|
protected readonly environmentService: EnvironmentService,
|
||||||
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||||
|
protected readonly syncWorkspaceMetadataCommand: SyncWorkspaceMetadataCommand,
|
||||||
|
|
||||||
protected readonly migrateRichTextContentPatchCommand: MigrateRichTextContentPatchCommand,
|
protected readonly migrateRichTextContentPatchCommand: MigrateRichTextContentPatchCommand,
|
||||||
protected readonly addTasksAssignedToMeViewCommand: AddTasksAssignedToMeViewCommand,
|
protected readonly addTasksAssignedToMeViewCommand: AddTasksAssignedToMeViewCommand,
|
||||||
protected readonly migrateIsSearchableForCustomObjectMetadataCommand: MigrateIsSearchableForCustomObjectMetadataCommand,
|
protected readonly migrateIsSearchableForCustomObjectMetadataCommand: MigrateIsSearchableForCustomObjectMetadataCommand,
|
||||||
protected readonly updateDefaultViewRecordOpeningOnWorkflowObjectsCommand: UpdateDefaultViewRecordOpeningOnWorkflowObjectsCommand,
|
protected readonly updateDefaultViewRecordOpeningOnWorkflowObjectsCommand: UpdateDefaultViewRecordOpeningOnWorkflowObjectsCommand,
|
||||||
protected readonly migrateSearchVectorOnNoteAndTaskEntitiesCommand: MigrateSearchVectorOnNoteAndTaskEntitiesCommand,
|
protected readonly migrateSearchVectorOnNoteAndTaskEntitiesCommand: MigrateSearchVectorOnNoteAndTaskEntitiesCommand,
|
||||||
protected readonly syncWorkspaceMetadataCommand: SyncWorkspaceMetadataCommand,
|
|
||||||
) {
|
) {
|
||||||
super(workspaceRepository, twentyORMGlobalManager);
|
super(
|
||||||
|
workspaceRepository,
|
||||||
|
environmentService,
|
||||||
|
twentyORMGlobalManager,
|
||||||
|
syncWorkspaceMetadataCommand,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
override async runOnWorkspace(args: RunOnWorkspaceArgs): Promise<void> {
|
override async runBeforeSyncMetadata(args: RunOnWorkspaceArgs) {
|
||||||
this.logger.log(
|
|
||||||
chalk.blue(
|
|
||||||
`${args.options.dryRun ? '(dry run)' : ''} Upgrading workspace ${args.workspaceId} ${args.index + 1}/${args.total}`,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
await this.migrateRichTextContentPatchCommand.runOnWorkspace(args);
|
await this.migrateRichTextContentPatchCommand.runOnWorkspace(args);
|
||||||
|
|
||||||
await this.migrateIsSearchableForCustomObjectMetadataCommand.runOnWorkspace(
|
await this.migrateIsSearchableForCustomObjectMetadataCommand.runOnWorkspace(
|
||||||
@ -56,17 +58,13 @@ export class UpgradeCommand extends ActiveOrSuspendedWorkspacesMigrationCommandR
|
|||||||
await this.migrateIsSearchableForCustomObjectMetadataCommand.runOnWorkspace(
|
await this.migrateIsSearchableForCustomObjectMetadataCommand.runOnWorkspace(
|
||||||
args,
|
args,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
await this.syncWorkspaceMetadataCommand.runOnWorkspace(args);
|
override async runAfterSyncMetadata(args: RunOnWorkspaceArgs) {
|
||||||
|
|
||||||
await this.updateDefaultViewRecordOpeningOnWorkflowObjectsCommand.runOnWorkspace(
|
await this.updateDefaultViewRecordOpeningOnWorkflowObjectsCommand.runOnWorkspace(
|
||||||
args,
|
args,
|
||||||
);
|
);
|
||||||
|
|
||||||
await this.addTasksAssignedToMeViewCommand.runOnWorkspace(args);
|
await this.addTasksAssignedToMeViewCommand.runOnWorkspace(args);
|
||||||
|
|
||||||
this.logger.log(
|
|
||||||
chalk.blue(`Upgrade for workspace ${args.workspaceId} completed.`),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import {
|
|||||||
IsEnum,
|
IsEnum,
|
||||||
IsNumber,
|
IsNumber,
|
||||||
IsOptional,
|
IsOptional,
|
||||||
|
IsSemVer,
|
||||||
IsString,
|
IsString,
|
||||||
IsUrl,
|
IsUrl,
|
||||||
ValidateIf,
|
ValidateIf,
|
||||||
@ -978,6 +979,14 @@ export class EnvironmentVariables {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
IS_ATTACHMENT_PREVIEW_ENABLED = true;
|
IS_ATTACHMENT_PREVIEW_ENABLED = true;
|
||||||
|
|
||||||
|
@EnvironmentVariablesMetadata({
|
||||||
|
group: EnvironmentVariablesGroup.ServerConfig,
|
||||||
|
description: 'Twenty server version',
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsSemVer()
|
||||||
|
APP_VERSION?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const validate = (
|
export const validate = (
|
||||||
|
|||||||
@ -0,0 +1,7 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`is-same-major-and-minor-version incomplete version1 1`] = `"Received invalid version: 1.0 1.1.0"`;
|
||||||
|
|
||||||
|
exports[`is-same-major-and-minor-version invalid version1 1`] = `"Received invalid version: invalid 1.1.0"`;
|
||||||
|
|
||||||
|
exports[`is-same-major-and-minor-version invalid version2 1`] = `"Received invalid version: 1.0.0 invalid"`;
|
||||||
@ -0,0 +1,192 @@
|
|||||||
|
import { EachTestingContext } from 'twenty-shared';
|
||||||
|
|
||||||
|
import { compareVersionMajorAndMinor } from 'src/utils/version/compare-version-minor-and-major';
|
||||||
|
|
||||||
|
type IsSameVersionTestCase = EachTestingContext<{
|
||||||
|
version1: string;
|
||||||
|
version2: string;
|
||||||
|
expected?: ReturnType<typeof compareVersionMajorAndMinor>;
|
||||||
|
expectToThrow?: boolean;
|
||||||
|
}>;
|
||||||
|
describe('is-same-major-and-minor-version', () => {
|
||||||
|
const beneathVersionTestCases: IsSameVersionTestCase[] = [
|
||||||
|
{
|
||||||
|
context: {
|
||||||
|
version1: '1.0.0',
|
||||||
|
version2: '1.1.0',
|
||||||
|
expected: 'lower',
|
||||||
|
},
|
||||||
|
title: 'different minor version',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
context: {
|
||||||
|
version1: '2.3.0',
|
||||||
|
version2: '2.4.0',
|
||||||
|
expected: 'lower',
|
||||||
|
},
|
||||||
|
title: 'different minor version',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
context: {
|
||||||
|
version1: '0.1.0',
|
||||||
|
version2: '0.2.0',
|
||||||
|
expected: 'lower',
|
||||||
|
},
|
||||||
|
title: 'different minor version with zero major',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
context: {
|
||||||
|
version1: '2.3.5',
|
||||||
|
version2: '2.4.1',
|
||||||
|
expected: 'lower',
|
||||||
|
},
|
||||||
|
title: 'different minor and patch versions',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
context: {
|
||||||
|
version1: '1.0.0-alpha',
|
||||||
|
version2: '1.1.0-beta',
|
||||||
|
expected: 'lower',
|
||||||
|
},
|
||||||
|
title: 'different minor version with different pre-release tags',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
context: {
|
||||||
|
version1: 'v1.0.0',
|
||||||
|
version2: 'v1.1.0',
|
||||||
|
expected: 'lower',
|
||||||
|
},
|
||||||
|
title: 'different minor version with v prefix',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
context: {
|
||||||
|
version1: '2.0.0',
|
||||||
|
version2: '42.42.42',
|
||||||
|
expected: 'lower',
|
||||||
|
},
|
||||||
|
title: 'above version2',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
context: {
|
||||||
|
version1: '2.0.0',
|
||||||
|
version2: 'v42.42.42',
|
||||||
|
expected: 'lower',
|
||||||
|
},
|
||||||
|
title: 'above version2 with v-prefix',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const sameVersionTestCases: IsSameVersionTestCase[] = [
|
||||||
|
{
|
||||||
|
context: {
|
||||||
|
version1: '1.1.0',
|
||||||
|
version2: '1.1.0',
|
||||||
|
expected: 'equal',
|
||||||
|
},
|
||||||
|
title: 'exact same version',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
context: {
|
||||||
|
version1: '1.1.0',
|
||||||
|
version2: '1.1.42',
|
||||||
|
expected: 'equal',
|
||||||
|
},
|
||||||
|
title: 'exact same major and minor but different patch version',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
context: {
|
||||||
|
version1: 'v1.1.0',
|
||||||
|
version2: 'v1.1.0',
|
||||||
|
expected: 'equal',
|
||||||
|
},
|
||||||
|
title: 'exact same version with v prefix',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
context: {
|
||||||
|
version1: '1.1.0-alpha',
|
||||||
|
version2: '1.1.0-alpha',
|
||||||
|
expected: 'equal',
|
||||||
|
},
|
||||||
|
title: 'exact same version with same pre-release tag',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
context: {
|
||||||
|
version1: '0.0.1',
|
||||||
|
version2: '0.0.1',
|
||||||
|
expected: 'equal',
|
||||||
|
},
|
||||||
|
title: 'exact same version with all zeros',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
context: {
|
||||||
|
version1: 'v1.1.0',
|
||||||
|
version2: '1.1.0',
|
||||||
|
expected: 'equal',
|
||||||
|
},
|
||||||
|
title: 'same version with different v-prefix',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const aboveVersionTestCases: IsSameVersionTestCase[] = [
|
||||||
|
{
|
||||||
|
context: {
|
||||||
|
version1: 'v42.1.0',
|
||||||
|
version2: '2.0.0',
|
||||||
|
expected: 'higher',
|
||||||
|
},
|
||||||
|
title: 'above version',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
context: {
|
||||||
|
version1: '42.42.42',
|
||||||
|
version2: '2.0.0',
|
||||||
|
expected: 'higher',
|
||||||
|
},
|
||||||
|
title: 'above version with prefix',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const invalidTestCases: IsSameVersionTestCase[] = [
|
||||||
|
{
|
||||||
|
context: {
|
||||||
|
version1: 'invalid',
|
||||||
|
version2: '1.1.0',
|
||||||
|
expectToThrow: true,
|
||||||
|
},
|
||||||
|
title: 'invalid version1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
context: {
|
||||||
|
version1: '1.0.0',
|
||||||
|
version2: 'invalid',
|
||||||
|
expectToThrow: true,
|
||||||
|
},
|
||||||
|
title: 'invalid version2',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
context: {
|
||||||
|
version1: '1.0',
|
||||||
|
version2: '1.1.0',
|
||||||
|
expectToThrow: true,
|
||||||
|
},
|
||||||
|
title: 'incomplete version1',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
...sameVersionTestCases,
|
||||||
|
...invalidTestCases,
|
||||||
|
...beneathVersionTestCases,
|
||||||
|
...aboveVersionTestCases,
|
||||||
|
])(
|
||||||
|
'$title',
|
||||||
|
({ context: { version1, version2, expected, expectToThrow = false } }) => {
|
||||||
|
if (expectToThrow) {
|
||||||
|
expect(() =>
|
||||||
|
compareVersionMajorAndMinor(version1, version2),
|
||||||
|
).toThrowErrorMatchingSnapshot();
|
||||||
|
} else {
|
||||||
|
expect(compareVersionMajorAndMinor(version1, version2)).toBe(expected);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
@ -0,0 +1,37 @@
|
|||||||
|
import * as semver from 'semver';
|
||||||
|
|
||||||
|
type CompareVersionMajorAndMinorReturnType = 'lower' | 'equal' | 'higher';
|
||||||
|
export function compareVersionMajorAndMinor(
|
||||||
|
rawVersion1: string,
|
||||||
|
rawVersion2: string,
|
||||||
|
): CompareVersionMajorAndMinorReturnType {
|
||||||
|
const [version1, version2] = [rawVersion1, rawVersion2].map((version) =>
|
||||||
|
semver.parse(version),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (version1 === null || version2 === null) {
|
||||||
|
throw new Error(`Received invalid version: ${rawVersion1} ${rawVersion2}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const v1WithoutPatch = `${version1.major}.${version1.minor}.0`;
|
||||||
|
const v2WithoutPatch = `${version2.major}.${version2.minor}.0`;
|
||||||
|
|
||||||
|
const compareResult = semver.compare(v1WithoutPatch, v2WithoutPatch);
|
||||||
|
|
||||||
|
switch (compareResult) {
|
||||||
|
case -1: {
|
||||||
|
return 'lower';
|
||||||
|
}
|
||||||
|
case 0: {
|
||||||
|
return 'equal';
|
||||||
|
}
|
||||||
|
case 1: {
|
||||||
|
return 'higher';
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
throw new Error(
|
||||||
|
`Should never occur, encountered an unexpected value from semver.compare ${compareResult}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -161,7 +161,7 @@ yarn command:prod cron:calendar:ongoing-stale
|
|||||||
['SERVER_URL', 'http://localhost:3000', 'Url to the hosted server'],
|
['SERVER_URL', 'http://localhost:3000', 'Url to the hosted server'],
|
||||||
['FRONTEND_URL', '$SERVER_URL', 'Url to the frontend server. Same as SERVER_URL by default'],
|
['FRONTEND_URL', '$SERVER_URL', 'Url to the frontend server. Same as SERVER_URL by default'],
|
||||||
['PORT', '3000', 'Port of the backend server'],
|
['PORT', '3000', 'Port of the backend server'],
|
||||||
['CACHE_STORAGE_TTL', '3600 * 24 * 7', 'Cache TTL in seconds']
|
['CACHE_STORAGE_TTL', '3600 * 24 * 7', 'Cache TTL in seconds'],
|
||||||
]}></ArticleTable>
|
]}></ArticleTable>
|
||||||
|
|
||||||
### Security
|
### Security
|
||||||
|
|||||||
Reference in New Issue
Block a user