Compare commits

...

1 Commits

Author SHA1 Message Date
prastoin 1a15dcc0f2 feat(server): new decorator and template and service 2026-04-06 10:16:04 +02:00
12 changed files with 574 additions and 47 deletions
@@ -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'>;
@@ -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`,
);
}
}
@@ -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,
};
}
@@ -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",
@@ -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();
});
});
@@ -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']);
});
});
@@ -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
}
}
`;
}
}
@@ -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> {
@@ -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 ?? [];
}
}
@@ -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}`);
}
}
@@ -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': [],
};
}
}
@@ -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);