Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1a15dcc0f2 |
+73
-2
@@ -15,7 +15,9 @@ import {
|
||||
UpgradeCommandOptions,
|
||||
UpgradeCommandRunner,
|
||||
type AllCommands,
|
||||
type AllSlowCommands,
|
||||
} from 'src/database/commands/command-runners/upgrade.command-runner';
|
||||
import { SlowCoreMigrationCommandRunner } from 'src/database/commands/command-runners/slow-core-migration.command-runner';
|
||||
import { WorkspaceIteratorService } from 'src/database/commands/command-runners/workspace-iterator.service';
|
||||
import { CoreMigrationRunnerService } from 'src/database/commands/core-migration/services/core-migration-runner.service';
|
||||
import { RegisteredCoreMigrationService } from 'src/database/commands/core-migration/services/registered-core-migration-registry.service';
|
||||
@@ -40,6 +42,10 @@ class BasicUpgradeCommandRunner extends UpgradeCommandRunner {
|
||||
allCommands = Object.fromEntries(
|
||||
UPGRADE_COMMAND_SUPPORTED_VERSIONS.map((version) => [version, []]),
|
||||
) as unknown as AllCommands;
|
||||
|
||||
allSlowCommands = Object.fromEntries(
|
||||
UPGRADE_COMMAND_SUPPORTED_VERSIONS.map((version) => [version, []]),
|
||||
) as unknown as AllSlowCommands;
|
||||
}
|
||||
|
||||
type CommandRunnerValues = typeof BasicUpgradeCommandRunner;
|
||||
@@ -91,7 +97,8 @@ const buildUpgradeCommandModule = async ({
|
||||
: {
|
||||
provide: RegisteredCoreMigrationService,
|
||||
useValue: {
|
||||
getInstanceCommandsForVersion: jest.fn().mockReturnValue([]),
|
||||
getFastInstanceCommandsForVersion: jest.fn().mockReturnValue([]),
|
||||
getSlowInstanceCommandsForVersion: jest.fn().mockReturnValue([]),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -161,6 +168,7 @@ const buildUpgradeCommandModule = async ({
|
||||
runSingleMigration: jest
|
||||
.fn()
|
||||
.mockResolvedValue({ status: 'success' }),
|
||||
isMigrationPending: jest.fn().mockResolvedValue(true),
|
||||
},
|
||||
},
|
||||
registryProvider,
|
||||
@@ -346,7 +354,7 @@ describe('UpgradeCommandRunner', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should only run instance commands for the current version', async () => {
|
||||
it('should only run fast instance commands for the current version', async () => {
|
||||
@RegisteredCoreMigration(CURRENT_VERSION)
|
||||
class AddIndexToUsers1770000000000 implements MigrationInterface {
|
||||
async up(_queryRunner: QueryRunner) {}
|
||||
@@ -397,6 +405,69 @@ describe('UpgradeCommandRunner', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should run slow commands after fast migrations and before workspace commands', async () => {
|
||||
const executionOrder: string[] = [];
|
||||
|
||||
@RegisteredCoreMigration(CURRENT_VERSION)
|
||||
class FastMigration1770000000000 implements MigrationInterface {
|
||||
async up(_queryRunner: QueryRunner) {}
|
||||
async down(_queryRunner: QueryRunner) {}
|
||||
}
|
||||
|
||||
class TestSlowCommand extends SlowCoreMigrationCommandRunner {
|
||||
async runDataMigration(): Promise<void> {
|
||||
executionOrder.push('slow-data-migration');
|
||||
}
|
||||
}
|
||||
|
||||
const module = await buildModuleAndSetupSpies({
|
||||
migrations: [new FastMigration1770000000000()],
|
||||
});
|
||||
|
||||
const migrationRunnerService = module.get(CoreMigrationRunnerService);
|
||||
|
||||
(migrationRunnerService.runSingleMigration as jest.Mock).mockImplementation(
|
||||
async (name: string) => {
|
||||
executionOrder.push(`runSingleMigration:${name}`);
|
||||
|
||||
return { status: 'success' };
|
||||
},
|
||||
);
|
||||
|
||||
(
|
||||
migrationRunnerService.isMigrationPending as jest.Mock
|
||||
)?.mockResolvedValue?.(true);
|
||||
|
||||
const slowCommand = new TestSlowCommand(
|
||||
migrationRunnerService,
|
||||
'SlowMigration1773000000000',
|
||||
);
|
||||
|
||||
jest.spyOn(slowCommand['logger'], 'log').mockImplementation();
|
||||
jest.spyOn(slowCommand['logger'], 'warn').mockImplementation();
|
||||
|
||||
upgradeCommandRunner.allSlowCommands[CURRENT_VERSION] = [slowCommand];
|
||||
|
||||
const iteratorService = module.get(WorkspaceIteratorService);
|
||||
|
||||
(iteratorService.iterate as jest.Mock).mockImplementation(
|
||||
async (args: { callback: (context: unknown) => Promise<void> }) => {
|
||||
executionOrder.push('workspace-iterator');
|
||||
|
||||
return { success: [], fail: [] };
|
||||
},
|
||||
);
|
||||
|
||||
await upgradeCommandRunner.run([], {});
|
||||
|
||||
expect(executionOrder).toStrictEqual([
|
||||
'runSingleMigration:FastMigration1770000000000',
|
||||
'slow-data-migration',
|
||||
'runSingleMigration:SlowMigration1773000000000',
|
||||
'workspace-iterator',
|
||||
]);
|
||||
});
|
||||
|
||||
describe('Workspace upgrade should fail', () => {
|
||||
const failingTestUseCases: EachTestingContext<{
|
||||
input: Omit<BuildModuleAndSetupSpiesArgs, 'numberOfWorkspace'>;
|
||||
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
import { CommandRunner } from 'nest-commander';
|
||||
|
||||
import { CoreMigrationRunnerService } from 'src/database/commands/core-migration/services/core-migration-runner.service';
|
||||
import { CommandLogger } from 'src/database/commands/logger';
|
||||
|
||||
export abstract class SlowCoreMigrationCommandRunner extends CommandRunner {
|
||||
protected logger: CommandLogger;
|
||||
|
||||
constructor(
|
||||
protected readonly coreMigrationRunnerService: CoreMigrationRunnerService,
|
||||
protected readonly migrationName: string,
|
||||
) {
|
||||
super();
|
||||
this.logger = new CommandLogger({
|
||||
verbose: false,
|
||||
constructorName: this.constructor.name,
|
||||
});
|
||||
}
|
||||
|
||||
abstract runDataMigration(): Promise<void>;
|
||||
|
||||
override async run(): Promise<void> {
|
||||
const isPending =
|
||||
await this.coreMigrationRunnerService.isMigrationPending(
|
||||
this.migrationName,
|
||||
);
|
||||
|
||||
if (!isPending) {
|
||||
this.logger.warn(
|
||||
`Slow migration ${this.migrationName} already executed, skipping`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.log(`Running data migration for ${this.migrationName}...`);
|
||||
await this.runDataMigration();
|
||||
|
||||
this.logger.log(`Running TypeORM migration ${this.migrationName}...`);
|
||||
const result =
|
||||
await this.coreMigrationRunnerService.runSingleMigration(
|
||||
this.migrationName,
|
||||
);
|
||||
|
||||
if (result.status === 'fail') {
|
||||
throw new Error(
|
||||
`Slow migration ${this.migrationName} failed: ${result.code}`,
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Slow migration ${this.migrationName} completed successfully`,
|
||||
);
|
||||
}
|
||||
}
|
||||
+26
-11
@@ -7,6 +7,7 @@ import { assertUnreachable, isDefined } from 'twenty-shared/utils';
|
||||
import { MigrationInterface, Repository } from 'typeorm';
|
||||
|
||||
import { ActiveOrSuspendedWorkspaceCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspace.command-runner';
|
||||
import { SlowCoreMigrationCommandRunner } from 'src/database/commands/command-runners/slow-core-migration.command-runner';
|
||||
import {
|
||||
type WorkspaceIteratorContext,
|
||||
WorkspaceIteratorService,
|
||||
@@ -33,6 +34,9 @@ export type VersionCommands = (
|
||||
)[];
|
||||
export type AllCommands = Record<UpgradeCommandVersion, VersionCommands>;
|
||||
|
||||
export type SlowCommands = SlowCoreMigrationCommandRunner[];
|
||||
export type AllSlowCommands = Record<UpgradeCommandVersion, SlowCommands>;
|
||||
|
||||
export type UpgradeCommandOptions = {
|
||||
workspaceId?: Set<string>;
|
||||
startFromWorkspaceId?: string;
|
||||
@@ -45,7 +49,8 @@ type VersionContext = {
|
||||
fromWorkspaceVersion: SemVer;
|
||||
currentAppVersion: SemVer;
|
||||
currentVersionMajorMinor: UpgradeCommandVersion;
|
||||
instanceCommands: MigrationInterface[];
|
||||
fastInstanceCommands: MigrationInterface[];
|
||||
slowInstanceCommands: SlowCommands;
|
||||
workspaceCommands: VersionCommands;
|
||||
};
|
||||
|
||||
@@ -53,6 +58,7 @@ export abstract class UpgradeCommandRunner extends CommandRunner {
|
||||
protected logger: CommandLogger;
|
||||
|
||||
public abstract allCommands: AllCommands;
|
||||
public abstract allSlowCommands: AllSlowCommands;
|
||||
|
||||
constructor(
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
@@ -152,7 +158,8 @@ export abstract class UpgradeCommandRunner extends CommandRunner {
|
||||
'Initialized upgrade context with:',
|
||||
`- currentVersion (migrating to): ${versionContext.currentAppVersion}`,
|
||||
`- fromWorkspaceVersion: ${versionContext.fromWorkspaceVersion}`,
|
||||
`- ${versionContext.instanceCommands.length} instance commands (from registry)`,
|
||||
`- ${versionContext.fastInstanceCommands.length} fast instance commands (from registry)`,
|
||||
`- ${versionContext.slowInstanceCommands.length} slow instance commands`,
|
||||
`- ${versionContext.workspaceCommands.length} workspace commands`,
|
||||
].join('\n '),
|
||||
),
|
||||
@@ -186,8 +193,8 @@ Please roll back to that version and run the upgrade command again.`,
|
||||
);
|
||||
}
|
||||
|
||||
for (const instanceCommand of versionContext.instanceCommands) {
|
||||
const migrationName = instanceCommand.constructor.name;
|
||||
for (const fastCommand of versionContext.fastInstanceCommands) {
|
||||
const migrationName = fastCommand.constructor.name;
|
||||
const result =
|
||||
await this.coreMigrationRunnerService.runSingleMigration(
|
||||
migrationName,
|
||||
@@ -196,14 +203,14 @@ Please roll back to that version and run the upgrade command again.`,
|
||||
if (result.status === 'fail') {
|
||||
if (result.code === 'already-executed') {
|
||||
this.logger.warn(
|
||||
`Core migration ${migrationName} already executed, skipping`,
|
||||
`Fast core migration ${migrationName} already executed, skipping`,
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
this.logger.error(
|
||||
`Core migration ${migrationName} failed with code: ${result.code}`,
|
||||
`Fast core migration ${migrationName} failed with code: ${result.code}`,
|
||||
);
|
||||
|
||||
if (isDefined(result.error)) {
|
||||
@@ -215,15 +222,19 @@ Please roll back to that version and run the upgrade command again.`,
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Core migration ${migrationName} failed: ${result.code}`,
|
||||
`Fast core migration ${migrationName} failed: ${result.code}`,
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Core migration ${migrationName} executed successfully`,
|
||||
`Fast core migration ${migrationName} executed successfully`,
|
||||
);
|
||||
}
|
||||
|
||||
for (const slowCommand of versionContext.slowInstanceCommands) {
|
||||
await slowCommand.run([], {});
|
||||
}
|
||||
|
||||
const iteratorReport = await this.workspaceIteratorService.iterate({
|
||||
workspaceIds:
|
||||
options.workspaceId && options.workspaceId.size > 0
|
||||
@@ -272,17 +283,21 @@ Please roll back to that version and run the upgrade command again.`,
|
||||
const fromWorkspaceVersion =
|
||||
this.coreEngineVersionService.getPreviousVersion();
|
||||
|
||||
const instanceCommands =
|
||||
this.versionedMigrationRegistryService.getInstanceCommandsForVersion(
|
||||
const fastInstanceCommands =
|
||||
this.versionedMigrationRegistryService.getFastInstanceCommandsForVersion(
|
||||
currentVersionMajorMinor,
|
||||
);
|
||||
|
||||
const slowInstanceCommands =
|
||||
this.allSlowCommands[currentVersionMajorMinor] ?? [];
|
||||
|
||||
return {
|
||||
fromWorkspaceVersion,
|
||||
currentAppVersion,
|
||||
currentVersionMajorMinor,
|
||||
workspaceCommands,
|
||||
instanceCommands,
|
||||
fastInstanceCommands,
|
||||
slowInstanceCommands,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+54
@@ -146,6 +146,60 @@ export class SeedSettingV12101775000000000 implements MigrationInterface {
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`CoreMigrationGeneratorService should generate a slow command template referencing the migration class 1`] = `
|
||||
{
|
||||
"fileName": "1-21-0-make-column-not-nullable-slow.command.ts",
|
||||
"fileTemplate": "import { Command } from 'nest-commander';
|
||||
|
||||
import { SlowCoreMigrationCommandRunner } from 'src/database/commands/command-runners/slow-core-migration.command-runner';
|
||||
import { CoreMigrationRunnerService } from 'src/database/commands/core-migration/services/core-migration-runner.service';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:1-21:make-column-not-nullable',
|
||||
description: 'Data migration + slow core migration: make-column-not-nullable',
|
||||
})
|
||||
export class MakeColumnNotNullableSlowCommand extends SlowCoreMigrationCommandRunner {
|
||||
constructor(
|
||||
protected readonly coreMigrationRunnerService: CoreMigrationRunnerService,
|
||||
) {
|
||||
super(
|
||||
coreMigrationRunnerService,
|
||||
'MakeColumnNotNullableV12101775000000000',
|
||||
);
|
||||
}
|
||||
|
||||
async runDataMigration(): Promise<void> {
|
||||
// TODO: implement data migration logic before the TypeORM migration runs
|
||||
}
|
||||
}
|
||||
",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`CoreMigrationGeneratorService should generate a slow migration template with TODO stubs 1`] = `
|
||||
{
|
||||
"className": "MakeColumnNotNullableV12101775000000000",
|
||||
"fileName": "1775000000000-1-21-0-make-column-not-nullable.ts",
|
||||
"fileTemplate": "import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
import { RegisteredCoreMigration } from 'src/database/typeorm/core/decorators/registered-core-migration.decorator';
|
||||
|
||||
@RegisteredCoreMigration('1.21.0', { type: 'slow' })
|
||||
export class MakeColumnNotNullableV12101775000000000 implements MigrationInterface {
|
||||
name = 'MakeColumnNotNullableV12101775000000000';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
// TODO: implement slow migration
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
// TODO: implement slow migration rollback
|
||||
}
|
||||
}
|
||||
",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`CoreMigrationGeneratorService should use default migration name in class and file names 1`] = `
|
||||
{
|
||||
"className": "AutoGeneratedV12101775000000000",
|
||||
|
||||
+25
@@ -177,4 +177,29 @@ describe('CoreMigrationGeneratorService', () => {
|
||||
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should generate a slow migration template with TODO stubs', async () => {
|
||||
const service = await buildService();
|
||||
|
||||
const result = service.generateSlowMigrationTemplate({
|
||||
migrationName: 'make-column-not-nullable',
|
||||
version: '1.21.0',
|
||||
timestamp: FIXED_TIMESTAMP,
|
||||
});
|
||||
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should generate a slow command template referencing the migration class', async () => {
|
||||
const service = await buildService();
|
||||
|
||||
const result = service.generateSlowCommandTemplate({
|
||||
migrationName: 'make-column-not-nullable',
|
||||
version: '1.21.0',
|
||||
timestamp: FIXED_TIMESTAMP,
|
||||
migrationClassName: 'MakeColumnNotNullableV12101775000000000',
|
||||
});
|
||||
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
+95
-11
@@ -40,6 +40,22 @@ class MigrationD1769000000000 implements MigrationInterface {
|
||||
async down(): Promise<void> {}
|
||||
}
|
||||
|
||||
@RegisteredCoreMigration('1.21.0', { type: 'slow' })
|
||||
class SlowMigrationE1773000000000 implements MigrationInterface {
|
||||
name = 'SlowMigrationE1773000000000';
|
||||
|
||||
async up(): Promise<void> {}
|
||||
async down(): Promise<void> {}
|
||||
}
|
||||
|
||||
@RegisteredCoreMigration('1.21.0', { type: 'slow' })
|
||||
class SlowMigrationF1774000000000 implements MigrationInterface {
|
||||
name = 'SlowMigrationF1774000000000';
|
||||
|
||||
async up(): Promise<void> {}
|
||||
async down(): Promise<void> {}
|
||||
}
|
||||
|
||||
class UndecoratedMigration1768000000000 implements MigrationInterface {
|
||||
name = 'UndecoratedMigration1768000000000';
|
||||
|
||||
@@ -68,7 +84,7 @@ const buildRegistryService = async (
|
||||
};
|
||||
|
||||
describe('VersionedMigrationRegistryService', () => {
|
||||
it('should group migrations by version', async () => {
|
||||
it('should group fast migrations by version', async () => {
|
||||
const service = await buildRegistryService([
|
||||
new MigrationD1769000000000(),
|
||||
new MigrationA1770000000000(),
|
||||
@@ -76,8 +92,8 @@ describe('VersionedMigrationRegistryService', () => {
|
||||
new MigrationC1772000000000(),
|
||||
]);
|
||||
|
||||
const v120 = service.getInstanceCommandsForVersion('1.20.0');
|
||||
const v121 = service.getInstanceCommandsForVersion('1.21.0');
|
||||
const v120 = service.getFastInstanceCommandsForVersion('1.20.0');
|
||||
const v121 = service.getFastInstanceCommandsForVersion('1.21.0');
|
||||
|
||||
expect(v120.map((m) => m.constructor.name)).toStrictEqual([
|
||||
'MigrationD1769000000000',
|
||||
@@ -98,7 +114,7 @@ describe('VersionedMigrationRegistryService', () => {
|
||||
]);
|
||||
|
||||
const names = service
|
||||
.getInstanceCommandsForVersion('1.21.0')
|
||||
.getFastInstanceCommandsForVersion('1.21.0')
|
||||
.map((m) => m.constructor.name);
|
||||
|
||||
expect(names).toStrictEqual([
|
||||
@@ -114,25 +130,93 @@ describe('VersionedMigrationRegistryService', () => {
|
||||
new MigrationA1770000000000(),
|
||||
]);
|
||||
|
||||
const v121 = service.getInstanceCommandsForVersion('1.21.0');
|
||||
const v121Fast = service.getFastInstanceCommandsForVersion('1.21.0');
|
||||
|
||||
expect(v121).toHaveLength(1);
|
||||
expect(v121[0].constructor.name).toBe('MigrationA1770000000000');
|
||||
expect(v121Fast).toHaveLength(1);
|
||||
expect(v121Fast[0].constructor.name).toBe('MigrationA1770000000000');
|
||||
});
|
||||
|
||||
it('should return empty array for version with no migrations', async () => {
|
||||
const service = await buildRegistryService([]);
|
||||
|
||||
expect(service.getInstanceCommandsForVersion('1.19.0')).toStrictEqual([]);
|
||||
expect(service.getInstanceCommandsForVersion('1.20.0')).toStrictEqual([]);
|
||||
expect(service.getInstanceCommandsForVersion('1.21.0')).toStrictEqual([]);
|
||||
expect(
|
||||
service.getFastInstanceCommandsForVersion('1.19.0'),
|
||||
).toStrictEqual([]);
|
||||
expect(
|
||||
service.getFastInstanceCommandsForVersion('1.20.0'),
|
||||
).toStrictEqual([]);
|
||||
expect(
|
||||
service.getFastInstanceCommandsForVersion('1.21.0'),
|
||||
).toStrictEqual([]);
|
||||
expect(
|
||||
service.getSlowInstanceCommandsForVersion('1.21.0'),
|
||||
).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it('should return empty array for unsupported version', async () => {
|
||||
const service = await buildRegistryService([]);
|
||||
|
||||
expect(
|
||||
service.getInstanceCommandsForVersion('99.0.0' as unknown as '1.21.0'),
|
||||
service.getFastInstanceCommandsForVersion(
|
||||
'99.0.0' as unknown as '1.21.0',
|
||||
),
|
||||
).toStrictEqual([]);
|
||||
expect(
|
||||
service.getSlowInstanceCommandsForVersion(
|
||||
'99.0.0' as unknown as '1.21.0',
|
||||
),
|
||||
).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it('should separate slow migrations into slow bucket', async () => {
|
||||
const service = await buildRegistryService([
|
||||
new MigrationA1770000000000(),
|
||||
new SlowMigrationE1773000000000(),
|
||||
new SlowMigrationF1774000000000(),
|
||||
]);
|
||||
|
||||
const fast = service.getFastInstanceCommandsForVersion('1.21.0');
|
||||
const slow = service.getSlowInstanceCommandsForVersion('1.21.0');
|
||||
|
||||
expect(fast.map((m) => m.constructor.name)).toStrictEqual([
|
||||
'MigrationA1770000000000',
|
||||
]);
|
||||
|
||||
expect(slow.map((m) => m.constructor.name)).toStrictEqual([
|
||||
'SlowMigrationE1773000000000',
|
||||
'SlowMigrationF1774000000000',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle mixed fast and slow migrations across versions', async () => {
|
||||
const service = await buildRegistryService([
|
||||
new MigrationD1769000000000(),
|
||||
new MigrationA1770000000000(),
|
||||
new SlowMigrationE1773000000000(),
|
||||
]);
|
||||
|
||||
expect(
|
||||
service
|
||||
.getFastInstanceCommandsForVersion('1.20.0')
|
||||
.map((m) => m.constructor.name),
|
||||
).toStrictEqual(['MigrationD1769000000000']);
|
||||
|
||||
expect(
|
||||
service
|
||||
.getSlowInstanceCommandsForVersion('1.20.0')
|
||||
.map((m) => m.constructor.name),
|
||||
).toStrictEqual([]);
|
||||
|
||||
expect(
|
||||
service
|
||||
.getFastInstanceCommandsForVersion('1.21.0')
|
||||
.map((m) => m.constructor.name),
|
||||
).toStrictEqual(['MigrationA1770000000000']);
|
||||
|
||||
expect(
|
||||
service
|
||||
.getSlowInstanceCommandsForVersion('1.21.0')
|
||||
.map((m) => m.constructor.name),
|
||||
).toStrictEqual(['SlowMigrationE1773000000000']);
|
||||
});
|
||||
});
|
||||
|
||||
+96
-3
@@ -18,6 +18,11 @@ export type GeneratedMigrationResult = {
|
||||
className: string;
|
||||
};
|
||||
|
||||
export type GeneratedSlowCommandResult = {
|
||||
fileName: string;
|
||||
fileTemplate: string;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class CoreMigrationGeneratorService {
|
||||
constructor(
|
||||
@@ -52,18 +57,75 @@ export class CoreMigrationGeneratorService {
|
||||
` await queryRunner.query('${this.escapeForSingleQuotedString(query)}'${this.formatQueryParams(parameters)});`,
|
||||
);
|
||||
|
||||
const fileTemplate = this.buildMigrationFileContent(
|
||||
const fileTemplate = this.buildFastMigrationFileContent(
|
||||
className,
|
||||
version,
|
||||
upStatements,
|
||||
downStatements,
|
||||
);
|
||||
|
||||
const fileName = `${timestamp}-${version.replace(/\./g, '-')}-${migrationName}.ts`;
|
||||
const fileName = this.buildFileName(migrationName, version, timestamp);
|
||||
|
||||
return { fileName, fileTemplate, className };
|
||||
}
|
||||
|
||||
generateSlowMigrationTemplate({
|
||||
migrationName,
|
||||
version,
|
||||
timestamp,
|
||||
}: GenerateMigrationArgs): GeneratedMigrationResult {
|
||||
const className = this.buildClassName(migrationName, version, timestamp);
|
||||
const fileName = this.buildFileName(migrationName, version, timestamp);
|
||||
const fileTemplate = this.buildSlowMigrationFileContent(
|
||||
className,
|
||||
version,
|
||||
);
|
||||
|
||||
return { fileName, fileTemplate, className };
|
||||
}
|
||||
|
||||
generateSlowCommandTemplate({
|
||||
migrationName,
|
||||
version,
|
||||
timestamp,
|
||||
migrationClassName,
|
||||
}: GenerateMigrationArgs & {
|
||||
migrationClassName: string;
|
||||
}): GeneratedSlowCommandResult {
|
||||
const versionDashed = version.replace(/\./g, '-');
|
||||
const versionShort = versionDashed.replace(/-0$/, '');
|
||||
const commandName = `upgrade:${versionShort}:${migrationName}`;
|
||||
const commandClassName = `${pascalCase(migrationName)}SlowCommand`;
|
||||
const fileName = `${versionDashed}-${migrationName}-slow.command.ts`;
|
||||
|
||||
const fileTemplate = `import { Command } from 'nest-commander';
|
||||
|
||||
import { SlowCoreMigrationCommandRunner } from 'src/database/commands/command-runners/slow-core-migration.command-runner';
|
||||
import { CoreMigrationRunnerService } from 'src/database/commands/core-migration/services/core-migration-runner.service';
|
||||
|
||||
@Command({
|
||||
name: '${commandName}',
|
||||
description: 'Data migration + slow core migration: ${migrationName}',
|
||||
})
|
||||
export class ${commandClassName} extends SlowCoreMigrationCommandRunner {
|
||||
constructor(
|
||||
protected readonly coreMigrationRunnerService: CoreMigrationRunnerService,
|
||||
) {
|
||||
super(
|
||||
coreMigrationRunnerService,
|
||||
'${migrationClassName}',
|
||||
);
|
||||
}
|
||||
|
||||
async runDataMigration(): Promise<void> {
|
||||
// TODO: implement data migration logic before the TypeORM migration runs
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
return { fileName, fileTemplate };
|
||||
}
|
||||
|
||||
private buildClassName(
|
||||
name: string,
|
||||
version: string,
|
||||
@@ -72,6 +134,14 @@ export class CoreMigrationGeneratorService {
|
||||
return `${pascalCase(name)}V${version.replace(/\./g, '')}${timestamp}`;
|
||||
}
|
||||
|
||||
private buildFileName(
|
||||
migrationName: string,
|
||||
version: string,
|
||||
timestamp: number,
|
||||
): string {
|
||||
return `${timestamp}-${version.replace(/\./g, '-')}-${migrationName}.ts`;
|
||||
}
|
||||
|
||||
private formatQueryParams(parameters: unknown[] | undefined): string {
|
||||
if (!parameters || !parameters.length) {
|
||||
return '';
|
||||
@@ -84,7 +154,7 @@ export class CoreMigrationGeneratorService {
|
||||
return query.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
||||
}
|
||||
|
||||
private buildMigrationFileContent(
|
||||
private buildFastMigrationFileContent(
|
||||
className: string,
|
||||
version: string,
|
||||
upStatements: string[],
|
||||
@@ -106,6 +176,29 @@ ${upStatements.join('\n')}
|
||||
${downStatements.join('\n')}
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
private buildSlowMigrationFileContent(
|
||||
className: string,
|
||||
version: string,
|
||||
): string {
|
||||
return `import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
import { RegisteredCoreMigration } from 'src/database/typeorm/core/decorators/registered-core-migration.decorator';
|
||||
|
||||
@RegisteredCoreMigration('${version}', { type: 'slow' })
|
||||
export class ${className} implements MigrationInterface {
|
||||
name = '${className}';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
// TODO: implement slow migration
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
// TODO: implement slow migration rollback
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
+9
@@ -45,6 +45,15 @@ export class CoreMigrationRunnerService {
|
||||
}
|
||||
}
|
||||
|
||||
async isMigrationPending(migrationName: string): Promise<boolean> {
|
||||
const migrationExecutor = new MigrationExecutor(this.dataSource);
|
||||
const pendingMigrations = await migrationExecutor.getPendingMigrations();
|
||||
|
||||
return pendingMigrations.some(
|
||||
(migration) => migration.name === migrationName,
|
||||
);
|
||||
}
|
||||
|
||||
async runSingleMigration(
|
||||
migrationName: string,
|
||||
): Promise<RunSingleMigrationResult> {
|
||||
|
||||
+31
-13
@@ -3,19 +3,24 @@ import { InjectDataSource } from '@nestjs/typeorm';
|
||||
|
||||
import { DataSource, type MigrationInterface } from 'typeorm';
|
||||
|
||||
import { getRegisteredCoreMigrationVersion } from 'src/database/typeorm/core/decorators/registered-core-migration.decorator';
|
||||
import {
|
||||
type CoreMigrationType,
|
||||
getRegisteredCoreMigrationMetadata,
|
||||
} from 'src/database/typeorm/core/decorators/registered-core-migration.decorator';
|
||||
import {
|
||||
UPGRADE_COMMAND_SUPPORTED_VERSIONS,
|
||||
type UpgradeCommandVersion,
|
||||
} from 'src/engine/constants/upgrade-command-supported-versions.constant';
|
||||
|
||||
type MigrationBucket = Record<CoreMigrationType, MigrationInterface[]>;
|
||||
|
||||
@Injectable()
|
||||
export class RegisteredCoreMigrationService implements OnModuleInit {
|
||||
private readonly logger = new Logger(RegisteredCoreMigrationService.name);
|
||||
|
||||
private readonly migrationsByVersion = new Map<
|
||||
UpgradeCommandVersion,
|
||||
MigrationInterface[]
|
||||
MigrationBucket
|
||||
>();
|
||||
|
||||
constructor(
|
||||
@@ -25,40 +30,53 @@ export class RegisteredCoreMigrationService implements OnModuleInit {
|
||||
|
||||
onModuleInit(): void {
|
||||
for (const version of UPGRADE_COMMAND_SUPPORTED_VERSIONS) {
|
||||
this.migrationsByVersion.set(version, []);
|
||||
this.migrationsByVersion.set(version, { fast: [], slow: [] });
|
||||
}
|
||||
|
||||
// dataSource.migrations is already sorted by timestamp (TypeORM sorts
|
||||
// ascending by the 13-digit suffix of the class name)
|
||||
for (const migration of this.dataSource.migrations) {
|
||||
const constructor = migration.constructor;
|
||||
const version = getRegisteredCoreMigrationVersion(constructor);
|
||||
const metadata = getRegisteredCoreMigrationMetadata(
|
||||
migration.constructor,
|
||||
);
|
||||
|
||||
if (version === undefined) {
|
||||
if (!metadata) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const bucket = this.migrationsByVersion.get(version);
|
||||
const bucket = this.migrationsByVersion.get(metadata.version);
|
||||
|
||||
if (!bucket) {
|
||||
continue;
|
||||
}
|
||||
|
||||
bucket.push(migration);
|
||||
bucket[metadata.type].push(migration);
|
||||
}
|
||||
|
||||
for (const [version, migrations] of this.migrationsByVersion) {
|
||||
if (migrations.length > 0) {
|
||||
for (const [version, bucket] of this.migrationsByVersion) {
|
||||
if (bucket.fast.length > 0) {
|
||||
this.logger.log(
|
||||
`Registered ${migrations.length} versioned migration(s) for ${version}: ${migrations.map((migration) => migration.constructor.name).join(', ')}`,
|
||||
`Registered ${bucket.fast.length} fast versioned migration(s) for ${version}: ${bucket.fast.map((migration) => migration.constructor.name).join(', ')}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (bucket.slow.length > 0) {
|
||||
this.logger.log(
|
||||
`Registered ${bucket.slow.length} slow versioned migration(s) for ${version}: ${bucket.slow.map((migration) => migration.constructor.name).join(', ')}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getInstanceCommandsForVersion(
|
||||
getFastInstanceCommandsForVersion(
|
||||
version: UpgradeCommandVersion,
|
||||
): MigrationInterface[] {
|
||||
return this.migrationsByVersion.get(version) ?? [];
|
||||
return this.migrationsByVersion.get(version)?.fast ?? [];
|
||||
}
|
||||
|
||||
getSlowInstanceCommandsForVersion(
|
||||
version: UpgradeCommandVersion,
|
||||
): MigrationInterface[] {
|
||||
return this.migrationsByVersion.get(version)?.slow ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
+82
-2
@@ -5,6 +5,7 @@ import { Logger } from '@nestjs/common';
|
||||
|
||||
import { Command, CommandRunner, Option } from 'nest-commander';
|
||||
|
||||
import { type CoreMigrationType } from 'src/database/typeorm/core/decorators/registered-core-migration.decorator';
|
||||
import { CoreMigrationGeneratorService } from 'src/database/commands/core-migration/services/core-migration-generator.service';
|
||||
import { UPGRADE_COMMAND_SUPPORTED_VERSIONS } from 'src/engine/constants/upgrade-command-supported-versions.constant';
|
||||
|
||||
@@ -13,8 +14,14 @@ const MIGRATIONS_DIR = path.resolve(
|
||||
'src/database/typeorm/core/migrations/common',
|
||||
);
|
||||
|
||||
const UPGRADE_COMMANDS_DIR = path.resolve(
|
||||
process.cwd(),
|
||||
'src/database/commands/upgrade-version-command',
|
||||
);
|
||||
|
||||
type GenerateVersionedMigrationCommandOptions = {
|
||||
name: string;
|
||||
type: CoreMigrationType;
|
||||
};
|
||||
|
||||
@Command({
|
||||
@@ -40,6 +47,19 @@ export class GenerateVersionedMigrationCommand extends CommandRunner {
|
||||
return value;
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags: '-t, --type <type>',
|
||||
description: 'Migration type: fast (auto-generated from schema diff) or slow (hand-written stub with command)',
|
||||
defaultValue: 'fast',
|
||||
})
|
||||
parseType(value: string): CoreMigrationType {
|
||||
if (value !== 'fast' && value !== 'slow') {
|
||||
throw new Error(`Invalid migration type "${value}". Must be "fast" or "slow".`);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
async run(
|
||||
_passedParams: string[],
|
||||
options: GenerateVersionedMigrationCommandOptions,
|
||||
@@ -52,10 +72,24 @@ export class GenerateVersionedMigrationCommand extends CommandRunner {
|
||||
throw new Error('No supported versions found');
|
||||
}
|
||||
|
||||
this.logger.log(`Generating versioned migration for version ${version}...`);
|
||||
|
||||
const timestamp = Date.now();
|
||||
|
||||
if (options.type === 'slow') {
|
||||
await this.generateSlowMigration(migrationName, version, timestamp);
|
||||
} else {
|
||||
await this.generateFastMigration(migrationName, version, timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
private async generateFastMigration(
|
||||
migrationName: string,
|
||||
version: (typeof UPGRADE_COMMAND_SUPPORTED_VERSIONS)[number],
|
||||
timestamp: number,
|
||||
): Promise<void> {
|
||||
this.logger.log(
|
||||
`Generating fast versioned migration for version ${version}...`,
|
||||
);
|
||||
|
||||
const result = await this.coreMigrationGeneratorService.generate({
|
||||
migrationName,
|
||||
version,
|
||||
@@ -78,4 +112,50 @@ export class GenerateVersionedMigrationCommand extends CommandRunner {
|
||||
this.logger.log(` Class: ${result.className}`);
|
||||
this.logger.log(` Version: ${version}`);
|
||||
}
|
||||
|
||||
private async generateSlowMigration(
|
||||
migrationName: string,
|
||||
version: (typeof UPGRADE_COMMAND_SUPPORTED_VERSIONS)[number],
|
||||
timestamp: number,
|
||||
): Promise<void> {
|
||||
this.logger.log(
|
||||
`Generating slow versioned migration for version ${version}...`,
|
||||
);
|
||||
|
||||
const migrationResult =
|
||||
this.coreMigrationGeneratorService.generateSlowMigrationTemplate({
|
||||
migrationName,
|
||||
version,
|
||||
timestamp,
|
||||
});
|
||||
|
||||
const migrationFilePath = path.join(
|
||||
MIGRATIONS_DIR,
|
||||
migrationResult.fileName,
|
||||
);
|
||||
|
||||
fs.writeFileSync(migrationFilePath, migrationResult.fileTemplate);
|
||||
|
||||
this.logger.log(
|
||||
`Slow migration stub generated: ${migrationFilePath}`,
|
||||
);
|
||||
|
||||
const commandResult =
|
||||
this.coreMigrationGeneratorService.generateSlowCommandTemplate({
|
||||
migrationName,
|
||||
version,
|
||||
timestamp,
|
||||
migrationClassName: migrationResult.className,
|
||||
});
|
||||
|
||||
const versionDashed = version.replace(/\./g, '-').replace(/-0$/, '');
|
||||
const commandDir = path.join(UPGRADE_COMMANDS_DIR, versionDashed);
|
||||
const commandFilePath = path.join(commandDir, commandResult.fileName);
|
||||
|
||||
fs.writeFileSync(commandFilePath, commandResult.fileTemplate);
|
||||
|
||||
this.logger.log(`Slow command stub generated: ${commandFilePath}`);
|
||||
this.logger.log(` Migration class: ${migrationResult.className}`);
|
||||
this.logger.log(` Version: ${version}`);
|
||||
}
|
||||
}
|
||||
|
||||
+8
@@ -5,6 +5,7 @@ import { type Repository } from 'typeorm';
|
||||
|
||||
import {
|
||||
type AllCommands,
|
||||
type AllSlowCommands,
|
||||
UpgradeCommandRunner,
|
||||
type VersionCommands,
|
||||
} from 'src/database/commands/command-runners/upgrade.command-runner';
|
||||
@@ -44,6 +45,7 @@ import { DropWorkspaceMessagingFksCommand } from 'src/database/commands/upgrade-
|
||||
})
|
||||
export class UpgradeCommand extends UpgradeCommandRunner {
|
||||
override allCommands: AllCommands;
|
||||
override allSlowCommands: AllSlowCommands;
|
||||
|
||||
constructor(
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
@@ -127,5 +129,11 @@ export class UpgradeCommand extends UpgradeCommandRunner {
|
||||
'1.20.0': commands_1200,
|
||||
'1.21.0': commands_1210,
|
||||
};
|
||||
|
||||
this.allSlowCommands = {
|
||||
'1.19.0': [],
|
||||
'1.20.0': [],
|
||||
'1.21.0': [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+20
-5
@@ -2,17 +2,32 @@ import 'reflect-metadata';
|
||||
|
||||
import { type UpgradeCommandVersion } from 'src/engine/constants/upgrade-command-supported-versions.constant';
|
||||
|
||||
const REGISTERED_CORE_MIGRATION_KEY = 'REGISTERED_CORE_MIGRATION_VERSION';
|
||||
const REGISTERED_CORE_MIGRATION_KEY = 'REGISTERED_CORE_MIGRATION';
|
||||
|
||||
export type CoreMigrationType = 'fast' | 'slow';
|
||||
|
||||
export type RegisteredCoreMigrationMetadata = {
|
||||
version: UpgradeCommandVersion;
|
||||
type: CoreMigrationType;
|
||||
};
|
||||
|
||||
// When dropping a version from UPGRADE_COMMAND_SUPPORTED_VERSIONS, also
|
||||
// remove the @RegisteredCoreMigration decorator from its associated migration files.
|
||||
export const RegisteredCoreMigration =
|
||||
(version: UpgradeCommandVersion): ClassDecorator =>
|
||||
(
|
||||
version: UpgradeCommandVersion,
|
||||
options?: { type: CoreMigrationType },
|
||||
): ClassDecorator =>
|
||||
(target) => {
|
||||
Reflect.defineMetadata(REGISTERED_CORE_MIGRATION_KEY, version, target);
|
||||
const metadata: RegisteredCoreMigrationMetadata = {
|
||||
version,
|
||||
type: options?.type ?? 'fast',
|
||||
};
|
||||
|
||||
Reflect.defineMetadata(REGISTERED_CORE_MIGRATION_KEY, metadata, target);
|
||||
};
|
||||
|
||||
export const getRegisteredCoreMigrationVersion = (
|
||||
export const getRegisteredCoreMigrationMetadata = (
|
||||
target: Function,
|
||||
): UpgradeCommandVersion | undefined =>
|
||||
): RegisteredCoreMigrationMetadata | undefined =>
|
||||
Reflect.getMetadata(REGISTERED_CORE_MIGRATION_KEY, target);
|
||||
|
||||
Reference in New Issue
Block a user