Prevent cross version upgrade mismatch in 1.22 (#19627)
## Introduction As the new upgrade sequence engine is released in `1.22` it requires all workspaces to be in `1.21.0` which mean they will have a cursor on the sequence As if if someone upgrades from `1.20` to `1.22` no `upgradeMigration` will exist and throw a pretty basic `Could not find any cursor, database might not been initialized correctly` Here we allow a meaningful error
This commit is contained in:
+16
-16
@@ -3,13 +3,13 @@
|
||||
exports[`InstanceCommandGenerationService should encode version correctly in file and class names 1`] = `
|
||||
{
|
||||
"className": "TestFastInstanceCommand",
|
||||
"fileName": "1-20-instance-command-fast-1775000000000-test.ts",
|
||||
"fileName": "1-21-instance-command-fast-1775000000000-test.ts",
|
||||
"fileTemplate": "import { QueryRunner } from 'typeorm';
|
||||
|
||||
import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator';
|
||||
import { FastInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/fast-instance-command.interface';
|
||||
|
||||
@RegisteredInstanceCommand('1.20.0', 1775000000000)
|
||||
@RegisteredInstanceCommand('1.21.0', 1775000000000)
|
||||
export class TestFastInstanceCommand implements FastInstanceCommand {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('SELECT 1');
|
||||
@@ -26,13 +26,13 @@ export class TestFastInstanceCommand implements FastInstanceCommand {
|
||||
exports[`InstanceCommandGenerationService should escape backslashes in SQL queries 1`] = `
|
||||
{
|
||||
"className": "UpdatePathFastInstanceCommand",
|
||||
"fileName": "1-21-instance-command-fast-1775000000000-update-path.ts",
|
||||
"fileName": "1-22-instance-command-fast-1775000000000-update-path.ts",
|
||||
"fileTemplate": "import { QueryRunner } from 'typeorm';
|
||||
|
||||
import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator';
|
||||
import { FastInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/fast-instance-command.interface';
|
||||
|
||||
@RegisteredInstanceCommand('1.21.0', 1775000000000)
|
||||
@RegisteredInstanceCommand('1.22.0', 1775000000000)
|
||||
export class UpdatePathFastInstanceCommand implements FastInstanceCommand {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('UPDATE "core"."config" SET "value" = E\\'path\\\\\\\\to\\\\\\\\file\\'');
|
||||
@@ -49,13 +49,13 @@ export class UpdatePathFastInstanceCommand implements FastInstanceCommand {
|
||||
exports[`InstanceCommandGenerationService should escape single quotes in SQL queries 1`] = `
|
||||
{
|
||||
"className": "UpdateConfigFastInstanceCommand",
|
||||
"fileName": "1-21-instance-command-fast-1775000000000-update-config.ts",
|
||||
"fileName": "1-22-instance-command-fast-1775000000000-update-config.ts",
|
||||
"fileTemplate": "import { QueryRunner } from 'typeorm';
|
||||
|
||||
import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator';
|
||||
import { FastInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/fast-instance-command.interface';
|
||||
|
||||
@RegisteredInstanceCommand('1.21.0', 1775000000000)
|
||||
@RegisteredInstanceCommand('1.22.0', 1775000000000)
|
||||
export class UpdateConfigFastInstanceCommand implements FastInstanceCommand {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('UPDATE "core"."config" SET "value" = \\'it\\'\\'s done\\'');
|
||||
@@ -72,13 +72,13 @@ export class UpdateConfigFastInstanceCommand implements FastInstanceCommand {
|
||||
exports[`InstanceCommandGenerationService should generate a migration with a single up/down query 1`] = `
|
||||
{
|
||||
"className": "AddFooColumnFastInstanceCommand",
|
||||
"fileName": "1-21-instance-command-fast-1775000000000-add-foo-column.ts",
|
||||
"fileName": "1-22-instance-command-fast-1775000000000-add-foo-column.ts",
|
||||
"fileTemplate": "import { QueryRunner } from 'typeorm';
|
||||
|
||||
import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator';
|
||||
import { FastInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/fast-instance-command.interface';
|
||||
|
||||
@RegisteredInstanceCommand('1.21.0', 1775000000000)
|
||||
@RegisteredInstanceCommand('1.22.0', 1775000000000)
|
||||
export class AddFooColumnFastInstanceCommand implements FastInstanceCommand {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('ALTER TABLE "core"."user" ADD "foo" varchar');
|
||||
@@ -95,13 +95,13 @@ export class AddFooColumnFastInstanceCommand implements FastInstanceCommand {
|
||||
exports[`InstanceCommandGenerationService should generate a migration with multiple queries 1`] = `
|
||||
{
|
||||
"className": "CreateTaskTableFastInstanceCommand",
|
||||
"fileName": "1-21-instance-command-fast-1775000000000-create-task-table.ts",
|
||||
"fileName": "1-22-instance-command-fast-1775000000000-create-task-table.ts",
|
||||
"fileTemplate": "import { QueryRunner } from 'typeorm';
|
||||
|
||||
import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator';
|
||||
import { FastInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/fast-instance-command.interface';
|
||||
|
||||
@RegisteredInstanceCommand('1.21.0', 1775000000000)
|
||||
@RegisteredInstanceCommand('1.22.0', 1775000000000)
|
||||
export class CreateTaskTableFastInstanceCommand implements FastInstanceCommand {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('CREATE TABLE "core"."task" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" varchar NOT NULL)');
|
||||
@@ -120,13 +120,13 @@ export class CreateTaskTableFastInstanceCommand implements FastInstanceCommand {
|
||||
exports[`InstanceCommandGenerationService should generate a migration with query parameters 1`] = `
|
||||
{
|
||||
"className": "SeedSettingFastInstanceCommand",
|
||||
"fileName": "1-21-instance-command-fast-1775000000000-seed-setting.ts",
|
||||
"fileName": "1-22-instance-command-fast-1775000000000-seed-setting.ts",
|
||||
"fileTemplate": "import { QueryRunner } from 'typeorm';
|
||||
|
||||
import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator';
|
||||
import { FastInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/fast-instance-command.interface';
|
||||
|
||||
@RegisteredInstanceCommand('1.21.0', 1775000000000)
|
||||
@RegisteredInstanceCommand('1.22.0', 1775000000000)
|
||||
export class SeedSettingFastInstanceCommand implements FastInstanceCommand {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('INSERT INTO "core"."setting" ("key", "value") VALUES ($1, $2)', ["theme","dark"]);
|
||||
@@ -143,13 +143,13 @@ export class SeedSettingFastInstanceCommand implements FastInstanceCommand {
|
||||
exports[`InstanceCommandGenerationService should generate a slow instance command with populated up/down 1`] = `
|
||||
{
|
||||
"className": "MakeColumnNotNullableSlowInstanceCommand",
|
||||
"fileName": "1-21-instance-command-slow-1775000000000-make-column-not-nullable.ts",
|
||||
"fileName": "1-22-instance-command-slow-1775000000000-make-column-not-nullable.ts",
|
||||
"fileTemplate": "import { DataSource, QueryRunner } from 'typeorm';
|
||||
|
||||
import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator';
|
||||
import { SlowInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/slow-instance-command.interface';
|
||||
|
||||
@RegisteredInstanceCommand('1.21.0', 1775000000000, { type: 'slow' })
|
||||
@RegisteredInstanceCommand('1.22.0', 1775000000000, { type: 'slow' })
|
||||
export class MakeColumnNotNullableSlowInstanceCommand implements SlowInstanceCommand {
|
||||
async runDataMigration(dataSource: DataSource): Promise<void> {
|
||||
// TODO: implement data backfill before the DDL migration
|
||||
@@ -170,13 +170,13 @@ export class MakeColumnNotNullableSlowInstanceCommand implements SlowInstanceCom
|
||||
exports[`InstanceCommandGenerationService should use default migration name in class and file names 1`] = `
|
||||
{
|
||||
"className": "AutoGeneratedFastInstanceCommand",
|
||||
"fileName": "1-21-instance-command-fast-1775000000000-auto-generated.ts",
|
||||
"fileName": "1-22-instance-command-fast-1775000000000-auto-generated.ts",
|
||||
"fileTemplate": "import { QueryRunner } from 'typeorm';
|
||||
|
||||
import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator';
|
||||
import { FastInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/fast-instance-command.interface';
|
||||
|
||||
@RegisteredInstanceCommand('1.21.0', 1775000000000)
|
||||
@RegisteredInstanceCommand('1.22.0', 1775000000000)
|
||||
export class AutoGeneratedFastInstanceCommand implements FastInstanceCommand {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('ALTER TABLE "core"."user" ADD "bar" integer');
|
||||
|
||||
+19
-12
@@ -2,6 +2,11 @@ import { Test } from '@nestjs/testing';
|
||||
import { getDataSourceToken } from '@nestjs/typeorm';
|
||||
|
||||
import { InstanceCommandGenerationService } from 'src/database/commands/instance-command-generation.service';
|
||||
import { TWENTY_CURRENT_VERSION } from 'src/engine/core-modules/upgrade/constants/twenty-current-version.constant';
|
||||
import { TWENTY_PREVIOUS_VERSIONS } from 'src/engine/core-modules/upgrade/constants/twenty-previous-versions.constant';
|
||||
|
||||
const VERSION_A = TWENTY_CURRENT_VERSION;
|
||||
const VERSION_B = TWENTY_PREVIOUS_VERSIONS[0];
|
||||
|
||||
const FIXED_TIMESTAMP = 1775000000000;
|
||||
|
||||
@@ -39,7 +44,7 @@ describe('InstanceCommandGenerationService', () => {
|
||||
|
||||
const result = await service.generateInstanceCommand({
|
||||
migrationName: 'no-changes',
|
||||
version: '1.21.0',
|
||||
version: VERSION_A,
|
||||
timestamp: FIXED_TIMESTAMP,
|
||||
});
|
||||
|
||||
@@ -54,7 +59,7 @@ describe('InstanceCommandGenerationService', () => {
|
||||
|
||||
const result = await service.generateInstanceCommand({
|
||||
migrationName: 'add-foo-column',
|
||||
version: '1.21.0',
|
||||
version: VERSION_A,
|
||||
timestamp: FIXED_TIMESTAMP,
|
||||
});
|
||||
|
||||
@@ -81,7 +86,7 @@ describe('InstanceCommandGenerationService', () => {
|
||||
|
||||
const result = await service.generateInstanceCommand({
|
||||
migrationName: 'create-task-table',
|
||||
version: '1.21.0',
|
||||
version: VERSION_A,
|
||||
timestamp: FIXED_TIMESTAMP,
|
||||
});
|
||||
|
||||
@@ -107,7 +112,7 @@ describe('InstanceCommandGenerationService', () => {
|
||||
|
||||
const result = await service.generateInstanceCommand({
|
||||
migrationName: 'seed-setting',
|
||||
version: '1.21.0',
|
||||
version: VERSION_A,
|
||||
timestamp: FIXED_TIMESTAMP,
|
||||
});
|
||||
|
||||
@@ -122,7 +127,7 @@ describe('InstanceCommandGenerationService', () => {
|
||||
|
||||
const result = await service.generateInstanceCommand({
|
||||
migrationName: 'update-config',
|
||||
version: '1.21.0',
|
||||
version: VERSION_A,
|
||||
timestamp: FIXED_TIMESTAMP,
|
||||
});
|
||||
|
||||
@@ -141,7 +146,7 @@ describe('InstanceCommandGenerationService', () => {
|
||||
|
||||
const result = await service.generateInstanceCommand({
|
||||
migrationName: 'update-path',
|
||||
version: '1.21.0',
|
||||
version: VERSION_A,
|
||||
timestamp: FIXED_TIMESTAMP,
|
||||
});
|
||||
|
||||
@@ -156,7 +161,7 @@ describe('InstanceCommandGenerationService', () => {
|
||||
|
||||
const result = await service.generateInstanceCommand({
|
||||
migrationName: 'auto-generated',
|
||||
version: '1.21.0',
|
||||
version: VERSION_A,
|
||||
timestamp: FIXED_TIMESTAMP,
|
||||
});
|
||||
|
||||
@@ -171,7 +176,7 @@ describe('InstanceCommandGenerationService', () => {
|
||||
|
||||
const result = await service.generateInstanceCommand({
|
||||
migrationName: 'test',
|
||||
version: '1.20.0',
|
||||
version: VERSION_B,
|
||||
timestamp: FIXED_TIMESTAMP,
|
||||
});
|
||||
|
||||
@@ -183,7 +188,7 @@ describe('InstanceCommandGenerationService', () => {
|
||||
|
||||
const result = await service.generateInstanceCommand({
|
||||
migrationName: 'no-changes',
|
||||
version: '1.21.0',
|
||||
version: VERSION_A,
|
||||
timestamp: FIXED_TIMESTAMP,
|
||||
type: 'slow',
|
||||
});
|
||||
@@ -207,7 +212,7 @@ describe('InstanceCommandGenerationService', () => {
|
||||
|
||||
const result = await service.generateInstanceCommand({
|
||||
migrationName: 'make-column-not-nullable',
|
||||
version: '1.21.0',
|
||||
version: VERSION_A,
|
||||
timestamp: FIXED_TIMESTAMP,
|
||||
type: 'slow',
|
||||
});
|
||||
@@ -223,13 +228,15 @@ describe('InstanceCommandGenerationService', () => {
|
||||
|
||||
const result = await service.generateInstanceCommand({
|
||||
migrationName: 'backfill-data',
|
||||
version: '1.20.0',
|
||||
version: VERSION_B,
|
||||
timestamp: FIXED_TIMESTAMP,
|
||||
type: 'slow',
|
||||
});
|
||||
|
||||
const versionSlug = VERSION_B.split('.').slice(0, 2).join('-');
|
||||
|
||||
expect(result?.fileName).toBe(
|
||||
'1-20-instance-command-slow-1775000000000-backfill-data.ts',
|
||||
`${versionSlug}-instance-command-slow-${FIXED_TIMESTAMP}-backfill-data.ts`,
|
||||
);
|
||||
expect(result?.className).toBe('BackfillDataSlowInstanceCommand');
|
||||
});
|
||||
|
||||
+49
-1
@@ -2,7 +2,8 @@ import { InjectDataSource } from '@nestjs/typeorm';
|
||||
|
||||
import chalk from 'chalk';
|
||||
import { Command, CommandRunner, Option } from 'nest-commander';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
|
||||
import { DataSource, In } from 'typeorm';
|
||||
|
||||
import { CommandLogger } from 'src/database/commands/logger';
|
||||
import { UpgradeCommandRegistryService } from 'src/engine/core-modules/upgrade/services/upgrade-command-registry.service';
|
||||
@@ -10,7 +11,9 @@ import { UpgradeMigrationService } from 'src/engine/core-modules/upgrade/service
|
||||
import { UpgradeSequenceReaderService } from 'src/engine/core-modules/upgrade/services/upgrade-sequence-reader.service';
|
||||
import { UpgradeSequenceRunnerService } from 'src/engine/core-modules/upgrade/services/upgrade-sequence-runner.service';
|
||||
import { RemovedSinceVersion } from 'src/engine/core-modules/upgrade/types/removed-since-version.type';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { WorkspaceVersionService } from 'src/engine/workspace-manager/workspace-version/services/workspace-version.service';
|
||||
import { compareVersionMajorAndMinor } from 'src/utils/version/compare-version-minor-and-major';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type RawUpgradeCommandOptions = {
|
||||
@@ -127,6 +130,7 @@ export class UpgradeCommand extends CommandRunner {
|
||||
|
||||
try {
|
||||
await this.runBootstrapMigrations();
|
||||
await this.guardAllActiveOrSuspendedWorkspacesAreIn1_21_0();
|
||||
await this.backfillWorkspaceCreatedIn1_21_0Cursors();
|
||||
|
||||
const sequence = this.upgradeSequenceReaderService.getUpgradeSequence();
|
||||
@@ -172,6 +176,50 @@ export class UpgradeCommand extends CommandRunner {
|
||||
}
|
||||
}
|
||||
|
||||
private async guardAllActiveOrSuspendedWorkspacesAreIn1_21_0(): RemovedSinceVersion<
|
||||
'1.23.0',
|
||||
Promise<void>
|
||||
> {
|
||||
const MINIMUM_VERSION = '1.21.0';
|
||||
|
||||
const activeOrSuspendedWorkspaces = await this.dataSource
|
||||
.getRepository(WorkspaceEntity)
|
||||
.find({
|
||||
select: {
|
||||
version: true,
|
||||
id: true,
|
||||
},
|
||||
where: {
|
||||
activationStatus: In([
|
||||
WorkspaceActivationStatus.ACTIVE,
|
||||
WorkspaceActivationStatus.SUSPENDED,
|
||||
]),
|
||||
},
|
||||
});
|
||||
|
||||
const workspacesBelowMinimum = activeOrSuspendedWorkspaces.filter(
|
||||
(workspace) =>
|
||||
!isDefined(workspace.version) ||
|
||||
compareVersionMajorAndMinor(workspace.version, MINIMUM_VERSION) ===
|
||||
'lower',
|
||||
);
|
||||
|
||||
if (workspacesBelowMinimum.length > 0) {
|
||||
const listing = workspacesBelowMinimum
|
||||
.map(
|
||||
(workspace) =>
|
||||
` - ${workspace.id} (version: ${workspace.version ?? 'null'})`,
|
||||
)
|
||||
.join('\n');
|
||||
|
||||
throw new Error(
|
||||
`Cannot upgrade: ${workspacesBelowMinimum.length} workspace(s) have a version below ${MINIMUM_VERSION}.\n` +
|
||||
`All workspaces must be upgraded to at least ${MINIMUM_VERSION} before running the 1.22.0 upgrade.\n` +
|
||||
`Affected workspaces:\n${listing}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Workspaces created during 1.21 were activated before the cursor-based
|
||||
// upgrade system existed. They have no upgradeMigration record yet.
|
||||
// Stamp them with the last 1.21 workspace command as their initial cursor.
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
export const TWENTY_PREVIOUS_VERSIONS = ['1.20.0', '1.21.0'] as const;
|
||||
export const TWENTY_PREVIOUS_VERSIONS = ['1.21.0'] as const;
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
exports[`UpgradeSequenceRunnerService — failing sequence (integration) should throw when cursor command is not found in the sequence 1`] = `"Step "RemovedCommand" not found in upgrade sequence. The sequence only covers versions [1.21.0, 1.22.0]. Please upgrade to 1.21.0 first."`;
|
||||
+1
-1
@@ -104,7 +104,7 @@ describe('UpgradeSequenceRunnerService — failing sequence (integration)', () =
|
||||
sequence,
|
||||
options: DEFAULT_OPTIONS,
|
||||
}),
|
||||
).rejects.toThrow('Step "RemovedCommand" not found in upgrade sequence');
|
||||
).rejects.toThrowErrorMatchingSnapshot();
|
||||
});
|
||||
|
||||
it('should throw when workspace cursors are outside the current slice', async () => {
|
||||
|
||||
+56
-47
@@ -11,8 +11,13 @@ import { UpgradeCommandRegistryService } from 'src/engine/core-modules/upgrade/s
|
||||
import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator';
|
||||
import { RegisteredWorkspaceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-workspace-command.decorator';
|
||||
import { type SlowInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/slow-instance-command.interface';
|
||||
import { TWENTY_CURRENT_VERSION } from 'src/engine/core-modules/upgrade/constants/twenty-current-version.constant';
|
||||
import { TWENTY_PREVIOUS_VERSIONS } from 'src/engine/core-modules/upgrade/constants/twenty-previous-versions.constant';
|
||||
|
||||
@RegisteredInstanceCommand('1.21.0', 1770000000000)
|
||||
const VERSION_A = TWENTY_CURRENT_VERSION;
|
||||
const VERSION_B = TWENTY_PREVIOUS_VERSIONS[0];
|
||||
|
||||
@RegisteredInstanceCommand(VERSION_A, 1770000000000)
|
||||
class MigrationA1770000000000 implements FastInstanceCommand {
|
||||
name = 'MigrationA1770000000000';
|
||||
|
||||
@@ -20,7 +25,7 @@ class MigrationA1770000000000 implements FastInstanceCommand {
|
||||
async down(): Promise<void> {}
|
||||
}
|
||||
|
||||
@RegisteredInstanceCommand('1.21.0', 1771000000000)
|
||||
@RegisteredInstanceCommand(VERSION_A, 1771000000000)
|
||||
class MigrationB1771000000000 implements FastInstanceCommand {
|
||||
name = 'MigrationB1771000000000';
|
||||
|
||||
@@ -28,7 +33,7 @@ class MigrationB1771000000000 implements FastInstanceCommand {
|
||||
async down(): Promise<void> {}
|
||||
}
|
||||
|
||||
@RegisteredInstanceCommand('1.21.0', 1772000000000)
|
||||
@RegisteredInstanceCommand(VERSION_A, 1772000000000)
|
||||
class MigrationC1772000000000 implements FastInstanceCommand {
|
||||
name = 'MigrationC1772000000000';
|
||||
|
||||
@@ -36,7 +41,7 @@ class MigrationC1772000000000 implements FastInstanceCommand {
|
||||
async down(): Promise<void> {}
|
||||
}
|
||||
|
||||
@RegisteredInstanceCommand('1.20.0', 1769000000000)
|
||||
@RegisteredInstanceCommand(VERSION_B, 1769000000000)
|
||||
class MigrationD1769000000000 implements FastInstanceCommand {
|
||||
name = 'MigrationD1769000000000';
|
||||
|
||||
@@ -51,12 +56,12 @@ class UndecoratedMigration1768000000000 implements FastInstanceCommand {
|
||||
async down(): Promise<void> {}
|
||||
}
|
||||
|
||||
@RegisteredWorkspaceCommand('1.21.0', 1773000000000)
|
||||
@RegisteredWorkspaceCommand(VERSION_A, 1773000000000)
|
||||
class WorkspaceCommandA {
|
||||
async runOnWorkspace(): Promise<void> {}
|
||||
}
|
||||
|
||||
@RegisteredWorkspaceCommand('1.21.0', 1774000000000)
|
||||
@RegisteredWorkspaceCommand(VERSION_A, 1774000000000)
|
||||
class WorkspaceCommandB {
|
||||
async runOnWorkspace(): Promise<void> {}
|
||||
}
|
||||
@@ -98,15 +103,19 @@ describe('UpgradeCommandRegistryService', () => {
|
||||
new WorkspaceCommandA(),
|
||||
]);
|
||||
|
||||
const v120 = service.getBundleForVersion('1.20.0');
|
||||
const v121 = service.getBundleForVersion('1.21.0');
|
||||
const bundleB = service.getBundleForVersion(VERSION_B);
|
||||
const bundleA = service.getBundleForVersion(VERSION_A);
|
||||
|
||||
expect(
|
||||
v120.fastInstanceCommands.map((entry) => entry.command.constructor.name),
|
||||
bundleB.fastInstanceCommands.map(
|
||||
(entry) => entry.command.constructor.name,
|
||||
),
|
||||
).toStrictEqual(['MigrationD1769000000000']);
|
||||
|
||||
expect(
|
||||
v121.fastInstanceCommands.map((entry) => entry.command.constructor.name),
|
||||
bundleA.fastInstanceCommands.map(
|
||||
(entry) => entry.command.constructor.name,
|
||||
),
|
||||
).toStrictEqual([
|
||||
'MigrationA1770000000000',
|
||||
'MigrationB1771000000000',
|
||||
@@ -123,7 +132,7 @@ describe('UpgradeCommandRegistryService', () => {
|
||||
]);
|
||||
|
||||
const names = service
|
||||
.getBundleForVersion('1.21.0')
|
||||
.getBundleForVersion(VERSION_A)
|
||||
.fastInstanceCommands.map((entry) => entry.command.constructor.name);
|
||||
|
||||
expect(names).toStrictEqual([
|
||||
@@ -140,10 +149,10 @@ describe('UpgradeCommandRegistryService', () => {
|
||||
new WorkspaceCommandA(),
|
||||
]);
|
||||
|
||||
const v121 = service.getBundleForVersion('1.21.0');
|
||||
const bundleA = service.getBundleForVersion(VERSION_A);
|
||||
|
||||
expect(v121.fastInstanceCommands).toHaveLength(1);
|
||||
expect(v121.fastInstanceCommands[0].command.constructor.name).toBe(
|
||||
expect(bundleA.fastInstanceCommands).toHaveLength(1);
|
||||
expect(bundleA.fastInstanceCommands[0].command.constructor.name).toBe(
|
||||
'MigrationA1770000000000',
|
||||
);
|
||||
});
|
||||
@@ -151,13 +160,13 @@ describe('UpgradeCommandRegistryService', () => {
|
||||
it('should return empty array for version with no commands', async () => {
|
||||
const service = await buildRegistryService([]);
|
||||
|
||||
const v120 = service.getBundleForVersion('1.20.0');
|
||||
const v121 = service.getBundleForVersion('1.21.0');
|
||||
const bundleB = service.getBundleForVersion(VERSION_B);
|
||||
const bundleA = service.getBundleForVersion(VERSION_A);
|
||||
|
||||
expect(v120.fastInstanceCommands).toStrictEqual([]);
|
||||
expect(v121.fastInstanceCommands).toStrictEqual([]);
|
||||
expect(v120.workspaceCommands).toStrictEqual([]);
|
||||
expect(v121.workspaceCommands).toStrictEqual([]);
|
||||
expect(bundleB.fastInstanceCommands).toStrictEqual([]);
|
||||
expect(bundleA.fastInstanceCommands).toStrictEqual([]);
|
||||
expect(bundleB.workspaceCommands).toStrictEqual([]);
|
||||
expect(bundleA.workspaceCommands).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it('should not throw when no commands are discovered (empty bundle)', async () => {
|
||||
@@ -168,7 +177,7 @@ describe('UpgradeCommandRegistryService', () => {
|
||||
const service = await buildRegistryService([]);
|
||||
|
||||
expect(
|
||||
service.getBundleForVersion('99.0.0' as unknown as '1.21.0')
|
||||
service.getBundleForVersion('99.0.0' as typeof VERSION_A)
|
||||
.fastInstanceCommands,
|
||||
).toStrictEqual([]);
|
||||
});
|
||||
@@ -179,7 +188,7 @@ describe('UpgradeCommandRegistryService', () => {
|
||||
new WorkspaceCommandA(),
|
||||
]);
|
||||
|
||||
const { workspaceCommands } = service.getBundleForVersion('1.21.0');
|
||||
const { workspaceCommands } = service.getBundleForVersion(VERSION_A);
|
||||
|
||||
expect(
|
||||
workspaceCommands.map((entry) => entry.command.constructor.name),
|
||||
@@ -194,14 +203,14 @@ describe('UpgradeCommandRegistryService', () => {
|
||||
new WorkspaceCommandB(),
|
||||
]);
|
||||
|
||||
const bucket = service.getBundleForVersion('1.21.0');
|
||||
const bucket = service.getBundleForVersion(VERSION_A);
|
||||
|
||||
expect(bucket.fastInstanceCommands).toHaveLength(2);
|
||||
expect(bucket.workspaceCommands).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should allow same timestamp across different kinds', async () => {
|
||||
@RegisteredWorkspaceCommand('1.21.0', 1770000000000)
|
||||
@RegisteredWorkspaceCommand(VERSION_A, 1770000000000)
|
||||
class WorkspaceCommandSameTimestamp {
|
||||
async runOnWorkspace(): Promise<void> {}
|
||||
}
|
||||
@@ -211,14 +220,14 @@ describe('UpgradeCommandRegistryService', () => {
|
||||
new WorkspaceCommandSameTimestamp(),
|
||||
]);
|
||||
|
||||
const bucket = service.getBundleForVersion('1.21.0');
|
||||
const bucket = service.getBundleForVersion(VERSION_A);
|
||||
|
||||
expect(bucket.fastInstanceCommands).toHaveLength(1);
|
||||
expect(bucket.workspaceCommands).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should throw on duplicate timestamps within the same kind', async () => {
|
||||
@RegisteredInstanceCommand('1.21.0', 1770000000000)
|
||||
@RegisteredInstanceCommand(VERSION_A, 1770000000000)
|
||||
class DuplicateInstanceTimestamp implements FastInstanceCommand {
|
||||
name = 'DuplicateInstanceTimestamp';
|
||||
|
||||
@@ -237,7 +246,7 @@ describe('UpgradeCommandRegistryService', () => {
|
||||
});
|
||||
|
||||
it('should throw on duplicate computed names across kinds', async () => {
|
||||
@RegisteredWorkspaceCommand('1.21.0', 1770000000000)
|
||||
@RegisteredWorkspaceCommand(VERSION_A, 1770000000000)
|
||||
class MigrationA1770000000000_WS {
|
||||
async runOnWorkspace(): Promise<void> {}
|
||||
}
|
||||
@@ -252,7 +261,7 @@ describe('UpgradeCommandRegistryService', () => {
|
||||
new MigrationA1770000000000_WS(),
|
||||
]),
|
||||
).rejects.toThrow(
|
||||
'Duplicate upgrade command name "1.21.0_MigrationA1770000000000_1770000000000"',
|
||||
`Duplicate upgrade command name "${VERSION_A}_MigrationA1770000000000_1770000000000"`,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -268,10 +277,10 @@ describe('UpgradeCommandRegistryService', () => {
|
||||
const allCommands = service.getCrossUpgradeSupportedFastInstanceCommands();
|
||||
|
||||
expect(allCommands.map((entry) => entry.name)).toStrictEqual([
|
||||
'1.20.0_MigrationD1769000000000_1769000000000',
|
||||
'1.21.0_MigrationA1770000000000_1770000000000',
|
||||
'1.21.0_MigrationB1771000000000_1771000000000',
|
||||
'1.21.0_MigrationC1772000000000_1772000000000',
|
||||
`${VERSION_B}_MigrationD1769000000000_1769000000000`,
|
||||
`${VERSION_A}_MigrationA1770000000000_1770000000000`,
|
||||
`${VERSION_A}_MigrationB1771000000000_1771000000000`,
|
||||
`${VERSION_A}_MigrationC1772000000000_1772000000000`,
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -284,7 +293,7 @@ describe('UpgradeCommandRegistryService', () => {
|
||||
});
|
||||
|
||||
it('should allow same class name with different timestamps across kinds', async () => {
|
||||
@RegisteredWorkspaceCommand('1.21.0', 1790000000000)
|
||||
@RegisteredWorkspaceCommand(VERSION_A, 1790000000000)
|
||||
class MigrationA1770000000000_WS {
|
||||
async runOnWorkspace(): Promise<void> {}
|
||||
}
|
||||
@@ -298,14 +307,14 @@ describe('UpgradeCommandRegistryService', () => {
|
||||
new MigrationA1770000000000_WS(),
|
||||
]);
|
||||
|
||||
const bucket = service.getBundleForVersion('1.21.0');
|
||||
const bucket = service.getBundleForVersion(VERSION_A);
|
||||
|
||||
expect(bucket.fastInstanceCommands).toHaveLength(1);
|
||||
expect(bucket.workspaceCommands).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should discover slow instance commands and sort by timestamp', async () => {
|
||||
@RegisteredInstanceCommand('1.21.0', 1780000000000, { type: 'slow' })
|
||||
@RegisteredInstanceCommand(VERSION_A, 1780000000000, { type: 'slow' })
|
||||
class SlowMigrationB1780000000000 implements SlowInstanceCommand {
|
||||
name = 'SlowMigrationB1780000000000';
|
||||
|
||||
@@ -314,7 +323,7 @@ describe('UpgradeCommandRegistryService', () => {
|
||||
async down(): Promise<void> {}
|
||||
}
|
||||
|
||||
@RegisteredInstanceCommand('1.21.0', 1779000000000, { type: 'slow' })
|
||||
@RegisteredInstanceCommand(VERSION_A, 1779000000000, { type: 'slow' })
|
||||
class SlowMigrationA1779000000000 implements SlowInstanceCommand {
|
||||
name = 'SlowMigrationA1779000000000';
|
||||
|
||||
@@ -329,7 +338,7 @@ describe('UpgradeCommandRegistryService', () => {
|
||||
new WorkspaceCommandA(),
|
||||
]);
|
||||
|
||||
const { slowInstanceCommands } = service.getBundleForVersion('1.21.0');
|
||||
const { slowInstanceCommands } = service.getBundleForVersion(VERSION_A);
|
||||
|
||||
expect(
|
||||
slowInstanceCommands.map((entry) => entry.command.constructor.name),
|
||||
@@ -340,7 +349,7 @@ describe('UpgradeCommandRegistryService', () => {
|
||||
});
|
||||
|
||||
it('should separate fast and slow instance commands in the same version', async () => {
|
||||
@RegisteredInstanceCommand('1.21.0', 1780000000000, { type: 'slow' })
|
||||
@RegisteredInstanceCommand(VERSION_A, 1780000000000, { type: 'slow' })
|
||||
class SlowMigration1780000000000 implements SlowInstanceCommand {
|
||||
name = 'SlowMigration1780000000000';
|
||||
|
||||
@@ -355,14 +364,14 @@ describe('UpgradeCommandRegistryService', () => {
|
||||
new WorkspaceCommandA(),
|
||||
]);
|
||||
|
||||
const bucket = service.getBundleForVersion('1.21.0');
|
||||
const bucket = service.getBundleForVersion(VERSION_A);
|
||||
|
||||
expect(bucket.fastInstanceCommands).toHaveLength(1);
|
||||
expect(bucket.slowInstanceCommands).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should throw on duplicate timestamps within slow instance commands', async () => {
|
||||
@RegisteredInstanceCommand('1.21.0', 1780000000000, { type: 'slow' })
|
||||
@RegisteredInstanceCommand(VERSION_A, 1780000000000, { type: 'slow' })
|
||||
class SlowMigrationA1780000000000 implements SlowInstanceCommand {
|
||||
name = 'SlowMigrationA1780000000000';
|
||||
|
||||
@@ -371,7 +380,7 @@ describe('UpgradeCommandRegistryService', () => {
|
||||
async down(): Promise<void> {}
|
||||
}
|
||||
|
||||
@RegisteredInstanceCommand('1.21.0', 1780000000000, { type: 'slow' })
|
||||
@RegisteredInstanceCommand(VERSION_A, 1780000000000, { type: 'slow' })
|
||||
class SlowMigrationB1780000000000 implements SlowInstanceCommand {
|
||||
name = 'SlowMigrationB1780000000000';
|
||||
|
||||
@@ -391,7 +400,7 @@ describe('UpgradeCommandRegistryService', () => {
|
||||
});
|
||||
|
||||
it('should allow same timestamp across fast and slow instance commands', async () => {
|
||||
@RegisteredInstanceCommand('1.21.0', 1770000000000, { type: 'slow' })
|
||||
@RegisteredInstanceCommand(VERSION_A, 1770000000000, { type: 'slow' })
|
||||
class SlowMigrationSameTimestamp implements SlowInstanceCommand {
|
||||
name = 'SlowMigrationSameTimestamp';
|
||||
|
||||
@@ -406,14 +415,14 @@ describe('UpgradeCommandRegistryService', () => {
|
||||
new WorkspaceCommandA(),
|
||||
]);
|
||||
|
||||
const bucket = service.getBundleForVersion('1.21.0');
|
||||
const bucket = service.getBundleForVersion(VERSION_A);
|
||||
|
||||
expect(bucket.fastInstanceCommands).toHaveLength(1);
|
||||
expect(bucket.slowInstanceCommands).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should return all slow instance commands across versions', async () => {
|
||||
@RegisteredInstanceCommand('1.21.0', 1780000000000, { type: 'slow' })
|
||||
@RegisteredInstanceCommand(VERSION_A, 1780000000000, { type: 'slow' })
|
||||
class SlowMigration1780000000000 implements SlowInstanceCommand {
|
||||
name = 'SlowMigration1780000000000';
|
||||
|
||||
@@ -422,7 +431,7 @@ describe('UpgradeCommandRegistryService', () => {
|
||||
async down(): Promise<void> {}
|
||||
}
|
||||
|
||||
@RegisteredInstanceCommand('1.20.0', 1768000000000, { type: 'slow' })
|
||||
@RegisteredInstanceCommand(VERSION_B, 1768000000000, { type: 'slow' })
|
||||
class SlowMigration1768000000000 implements SlowInstanceCommand {
|
||||
name = 'SlowMigration1768000000000';
|
||||
|
||||
@@ -441,8 +450,8 @@ describe('UpgradeCommandRegistryService', () => {
|
||||
service.getCrossUpgradeSupportedSlowInstanceCommands();
|
||||
|
||||
expect(allSlowCommands.map((entry) => entry.name)).toStrictEqual([
|
||||
'1.20.0_SlowMigration1768000000000_1768000000000',
|
||||
'1.21.0_SlowMigration1780000000000_1780000000000',
|
||||
`${VERSION_B}_SlowMigration1768000000000_1768000000000`,
|
||||
`${VERSION_A}_SlowMigration1780000000000_1780000000000`,
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
+8
-1
@@ -65,7 +65,14 @@ export class UpgradeSequenceReaderService {
|
||||
const cursor = sequence.findIndex((step) => step.name === stepName);
|
||||
|
||||
if (cursor === -1) {
|
||||
throw new Error(`Step "${stepName}" not found in upgrade sequence`);
|
||||
const supportedVersions =
|
||||
TWENTY_CROSS_UPGRADE_SUPPORTED_VERSIONS.join(', ');
|
||||
|
||||
throw new Error(
|
||||
`Step "${stepName}" not found in upgrade sequence. ` +
|
||||
`The sequence only covers versions [${supportedVersions}]. ` +
|
||||
`Please upgrade to ${TWENTY_CROSS_UPGRADE_SUPPORTED_VERSIONS[0]} first.`,
|
||||
);
|
||||
}
|
||||
|
||||
return cursor;
|
||||
|
||||
Reference in New Issue
Block a user