Workspace command decorators (#19397)
# Introduction Migrating the workspace commands to the decorator version + timestamp listing as for the instance commands We've now been able to remove the upgrade command abstraction where we needed to import all modules and order them Now they're dynamically retrieved at upgrade runtime, sorted by timestamp ## Instance and workspace commands name The name is computed from the command metadata `version` `className` and `timestamp` we have a duplicate validation at module init from the unified registry
This commit is contained in:
+31
@@ -0,0 +1,31 @@
|
||||
import 'reflect-metadata';
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { type UpgradeCommandVersion } from 'src/engine/constants/upgrade-command-supported-versions.constant';
|
||||
|
||||
export type RegisteredInstanceCommandMetadata = {
|
||||
version: UpgradeCommandVersion;
|
||||
timestamp: number;
|
||||
};
|
||||
|
||||
const REGISTERED_INSTANCE_COMMAND_KEY = 'REGISTERED_INSTANCE_COMMAND';
|
||||
|
||||
// When dropping a version from UPGRADE_COMMAND_SUPPORTED_VERSIONS, also
|
||||
// remove the @RegisteredInstanceCommand decorator from its associated
|
||||
// command files.
|
||||
export const RegisteredInstanceCommand =
|
||||
(version: UpgradeCommandVersion, timestamp: number): ClassDecorator =>
|
||||
(target) => {
|
||||
Injectable()(target);
|
||||
Reflect.defineMetadata(
|
||||
REGISTERED_INSTANCE_COMMAND_KEY,
|
||||
{ version, timestamp },
|
||||
target,
|
||||
);
|
||||
};
|
||||
|
||||
export const getRegisteredInstanceCommandMetadata = (
|
||||
target: Function,
|
||||
): RegisteredInstanceCommandMetadata | undefined =>
|
||||
Reflect.getMetadata(REGISTERED_INSTANCE_COMMAND_KEY, target);
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
import 'reflect-metadata';
|
||||
|
||||
import { type UpgradeCommandVersion } from 'src/engine/constants/upgrade-command-supported-versions.constant';
|
||||
|
||||
export type RegisteredWorkspaceCommandMetadata = {
|
||||
version: UpgradeCommandVersion;
|
||||
timestamp: number;
|
||||
};
|
||||
|
||||
const REGISTERED_WORKSPACE_COMMAND_KEY = 'REGISTERED_WORKSPACE_COMMAND';
|
||||
|
||||
export const RegisteredWorkspaceCommand =
|
||||
(version: UpgradeCommandVersion, timestamp: number): ClassDecorator =>
|
||||
(target) => {
|
||||
Reflect.defineMetadata(
|
||||
REGISTERED_WORKSPACE_COMMAND_KEY,
|
||||
{ version, timestamp },
|
||||
target,
|
||||
);
|
||||
};
|
||||
|
||||
export const getRegisteredWorkspaceCommandMetadata = (
|
||||
target: Function,
|
||||
): RegisteredWorkspaceCommandMetadata | undefined =>
|
||||
Reflect.getMetadata(REGISTERED_WORKSPACE_COMMAND_KEY, target);
|
||||
-145
@@ -1,145 +0,0 @@
|
||||
import 'reflect-metadata';
|
||||
|
||||
import { Test } from '@nestjs/testing';
|
||||
import { DiscoveryService } from '@nestjs/core';
|
||||
|
||||
import { type MigrationInterface } from 'typeorm';
|
||||
|
||||
import { RegisteredInstanceMigrationService } from 'src/engine/core-modules/upgrade/services/registered-instance-migration-registry.service';
|
||||
import { RegisteredInstanceMigration } from 'src/database/typeorm/core/decorators/registered-instance-migration.decorator';
|
||||
|
||||
@RegisteredInstanceMigration('1.21.0', 1770000000000)
|
||||
class MigrationA1770000000000 implements MigrationInterface {
|
||||
name = 'MigrationA1770000000000';
|
||||
|
||||
async up(): Promise<void> {}
|
||||
async down(): Promise<void> {}
|
||||
}
|
||||
|
||||
@RegisteredInstanceMigration('1.21.0', 1771000000000)
|
||||
class MigrationB1771000000000 implements MigrationInterface {
|
||||
name = 'MigrationB1771000000000';
|
||||
|
||||
async up(): Promise<void> {}
|
||||
async down(): Promise<void> {}
|
||||
}
|
||||
|
||||
@RegisteredInstanceMigration('1.21.0', 1772000000000)
|
||||
class MigrationC1772000000000 implements MigrationInterface {
|
||||
name = 'MigrationC1772000000000';
|
||||
|
||||
async up(): Promise<void> {}
|
||||
async down(): Promise<void> {}
|
||||
}
|
||||
|
||||
@RegisteredInstanceMigration('1.20.0', 1769000000000)
|
||||
class MigrationD1769000000000 implements MigrationInterface {
|
||||
name = 'MigrationD1769000000000';
|
||||
|
||||
async up(): Promise<void> {}
|
||||
async down(): Promise<void> {}
|
||||
}
|
||||
|
||||
class UndecoratedMigration1768000000000 implements MigrationInterface {
|
||||
name = 'UndecoratedMigration1768000000000';
|
||||
|
||||
async up(): Promise<void> {}
|
||||
async down(): Promise<void> {}
|
||||
}
|
||||
|
||||
const buildProviderWrapper = (migration: MigrationInterface) => ({
|
||||
instance: migration,
|
||||
metatype: migration.constructor,
|
||||
});
|
||||
|
||||
const buildRegistryService = async (
|
||||
migrations: MigrationInterface[],
|
||||
): Promise<RegisteredInstanceMigrationService> => {
|
||||
const module = await Test.createTestingModule({
|
||||
providers: [
|
||||
RegisteredInstanceMigrationService,
|
||||
{
|
||||
provide: DiscoveryService,
|
||||
useValue: {
|
||||
getProviders: () => migrations.map(buildProviderWrapper),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
const service = module.get(RegisteredInstanceMigrationService);
|
||||
|
||||
service.onModuleInit();
|
||||
|
||||
return service;
|
||||
};
|
||||
|
||||
describe('RegisteredInstanceMigrationService', () => {
|
||||
it('should group migrations by version', async () => {
|
||||
const service = await buildRegistryService([
|
||||
new MigrationD1769000000000(),
|
||||
new MigrationA1770000000000(),
|
||||
new MigrationB1771000000000(),
|
||||
new MigrationC1772000000000(),
|
||||
]);
|
||||
|
||||
const v120 = service.getInstanceCommandsForVersion('1.20.0');
|
||||
const v121 = service.getInstanceCommandsForVersion('1.21.0');
|
||||
|
||||
expect(v120.map((m) => m.constructor.name)).toStrictEqual([
|
||||
'MigrationD1769000000000',
|
||||
]);
|
||||
|
||||
expect(v121.map((m) => m.constructor.name)).toStrictEqual([
|
||||
'MigrationA1770000000000',
|
||||
'MigrationB1771000000000',
|
||||
'MigrationC1772000000000',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should sort migrations by timestamp within a version bucket', async () => {
|
||||
const service = await buildRegistryService([
|
||||
new MigrationC1772000000000(),
|
||||
new MigrationA1770000000000(),
|
||||
new MigrationB1771000000000(),
|
||||
]);
|
||||
|
||||
const names = service
|
||||
.getInstanceCommandsForVersion('1.21.0')
|
||||
.map((m) => m.constructor.name);
|
||||
|
||||
expect(names).toStrictEqual([
|
||||
'MigrationA1770000000000',
|
||||
'MigrationB1771000000000',
|
||||
'MigrationC1772000000000',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should skip undecorated migrations', async () => {
|
||||
const service = await buildRegistryService([
|
||||
new UndecoratedMigration1768000000000(),
|
||||
new MigrationA1770000000000(),
|
||||
]);
|
||||
|
||||
const v121 = service.getInstanceCommandsForVersion('1.21.0');
|
||||
|
||||
expect(v121).toHaveLength(1);
|
||||
expect(v121[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([]);
|
||||
});
|
||||
|
||||
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'),
|
||||
).toStrictEqual([]);
|
||||
});
|
||||
});
|
||||
+293
@@ -0,0 +1,293 @@
|
||||
import 'reflect-metadata';
|
||||
|
||||
import { Test } from '@nestjs/testing';
|
||||
import { DiscoveryService } from '@nestjs/core';
|
||||
|
||||
import { type MigrationInterface } from 'typeorm';
|
||||
|
||||
import { UpgradeCommandRegistryService } from 'src/engine/core-modules/upgrade/services/upgrade-command-registry.service';
|
||||
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';
|
||||
|
||||
@RegisteredInstanceCommand('1.21.0', 1770000000000)
|
||||
class MigrationA1770000000000 implements MigrationInterface {
|
||||
name = 'MigrationA1770000000000';
|
||||
|
||||
async up(): Promise<void> {}
|
||||
async down(): Promise<void> {}
|
||||
}
|
||||
|
||||
@RegisteredInstanceCommand('1.21.0', 1771000000000)
|
||||
class MigrationB1771000000000 implements MigrationInterface {
|
||||
name = 'MigrationB1771000000000';
|
||||
|
||||
async up(): Promise<void> {}
|
||||
async down(): Promise<void> {}
|
||||
}
|
||||
|
||||
@RegisteredInstanceCommand('1.21.0', 1772000000000)
|
||||
class MigrationC1772000000000 implements MigrationInterface {
|
||||
name = 'MigrationC1772000000000';
|
||||
|
||||
async up(): Promise<void> {}
|
||||
async down(): Promise<void> {}
|
||||
}
|
||||
|
||||
@RegisteredInstanceCommand('1.20.0', 1769000000000)
|
||||
class MigrationD1769000000000 implements MigrationInterface {
|
||||
name = 'MigrationD1769000000000';
|
||||
|
||||
async up(): Promise<void> {}
|
||||
async down(): Promise<void> {}
|
||||
}
|
||||
|
||||
class UndecoratedMigration1768000000000 implements MigrationInterface {
|
||||
name = 'UndecoratedMigration1768000000000';
|
||||
|
||||
async up(): Promise<void> {}
|
||||
async down(): Promise<void> {}
|
||||
}
|
||||
|
||||
@RegisteredWorkspaceCommand('1.21.0', 1773000000000)
|
||||
class WorkspaceCommandA {
|
||||
async runOnWorkspace(): Promise<void> {}
|
||||
}
|
||||
|
||||
@RegisteredWorkspaceCommand('1.21.0', 1774000000000)
|
||||
class WorkspaceCommandB {
|
||||
async runOnWorkspace(): Promise<void> {}
|
||||
}
|
||||
|
||||
const buildProviderWrapper = (instance: object) => ({
|
||||
instance,
|
||||
metatype: instance.constructor,
|
||||
});
|
||||
|
||||
const buildRegistryService = async (
|
||||
instances: object[],
|
||||
): Promise<UpgradeCommandRegistryService> => {
|
||||
const module = await Test.createTestingModule({
|
||||
providers: [
|
||||
UpgradeCommandRegistryService,
|
||||
{
|
||||
provide: DiscoveryService,
|
||||
useValue: {
|
||||
getProviders: () => instances.map(buildProviderWrapper),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
const service = module.get(UpgradeCommandRegistryService);
|
||||
|
||||
service.onModuleInit();
|
||||
|
||||
return service;
|
||||
};
|
||||
|
||||
describe('UpgradeCommandRegistryService', () => {
|
||||
it('should group instance migrations by version', async () => {
|
||||
const service = await buildRegistryService([
|
||||
new MigrationD1769000000000(),
|
||||
new MigrationA1770000000000(),
|
||||
new MigrationB1771000000000(),
|
||||
new MigrationC1772000000000(),
|
||||
]);
|
||||
|
||||
const v120 = service.getInstanceCommandsForVersion('1.20.0');
|
||||
const v121 = service.getInstanceCommandsForVersion('1.21.0');
|
||||
|
||||
expect(v120.map((migration) => migration.constructor.name)).toStrictEqual([
|
||||
'MigrationD1769000000000',
|
||||
]);
|
||||
|
||||
expect(v121.map((migration) => migration.constructor.name)).toStrictEqual([
|
||||
'MigrationA1770000000000',
|
||||
'MigrationB1771000000000',
|
||||
'MigrationC1772000000000',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should sort migrations by timestamp within a version bucket', async () => {
|
||||
const service = await buildRegistryService([
|
||||
new MigrationC1772000000000(),
|
||||
new MigrationA1770000000000(),
|
||||
new MigrationB1771000000000(),
|
||||
]);
|
||||
|
||||
const names = service
|
||||
.getInstanceCommandsForVersion('1.21.0')
|
||||
.map((migration) => migration.constructor.name);
|
||||
|
||||
expect(names).toStrictEqual([
|
||||
'MigrationA1770000000000',
|
||||
'MigrationB1771000000000',
|
||||
'MigrationC1772000000000',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should skip undecorated providers', async () => {
|
||||
const service = await buildRegistryService([
|
||||
new UndecoratedMigration1768000000000(),
|
||||
new MigrationA1770000000000(),
|
||||
]);
|
||||
|
||||
const v121 = service.getInstanceCommandsForVersion('1.21.0');
|
||||
|
||||
expect(v121).toHaveLength(1);
|
||||
expect(v121[0].constructor.name).toBe('MigrationA1770000000000');
|
||||
});
|
||||
|
||||
it('should return empty array for version with no commands', async () => {
|
||||
const service = await buildRegistryService([]);
|
||||
|
||||
expect(service.getInstanceCommandsForVersion('1.20.0')).toStrictEqual([]);
|
||||
expect(service.getInstanceCommandsForVersion('1.21.0')).toStrictEqual([]);
|
||||
expect(service.getWorkspaceCommandsForVersion('1.20.0')).toStrictEqual([]);
|
||||
expect(service.getWorkspaceCommandsForVersion('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'),
|
||||
).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it('should discover workspace commands and sort by timestamp', async () => {
|
||||
const service = await buildRegistryService([
|
||||
new WorkspaceCommandB(),
|
||||
new WorkspaceCommandA(),
|
||||
]);
|
||||
|
||||
const commands = service.getWorkspaceCommandsForVersion('1.21.0');
|
||||
|
||||
expect(commands.map((command) => command.constructor.name)).toStrictEqual([
|
||||
'WorkspaceCommandA',
|
||||
'WorkspaceCommandB',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should discover both instance and workspace commands for the same version', async () => {
|
||||
const service = await buildRegistryService([
|
||||
new MigrationA1770000000000(),
|
||||
new WorkspaceCommandA(),
|
||||
new MigrationB1771000000000(),
|
||||
new WorkspaceCommandB(),
|
||||
]);
|
||||
|
||||
const instanceCommands = service.getInstanceCommandsForVersion('1.21.0');
|
||||
const workspaceCommands = service.getWorkspaceCommandsForVersion('1.21.0');
|
||||
|
||||
expect(instanceCommands).toHaveLength(2);
|
||||
expect(workspaceCommands).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should allow same timestamp across different kinds', async () => {
|
||||
@RegisteredWorkspaceCommand('1.21.0', 1770000000000)
|
||||
class WorkspaceCommandSameTimestamp {
|
||||
async runOnWorkspace(): Promise<void> {}
|
||||
}
|
||||
|
||||
const service = await buildRegistryService([
|
||||
new MigrationA1770000000000(),
|
||||
new WorkspaceCommandSameTimestamp(),
|
||||
]);
|
||||
|
||||
expect(service.getInstanceCommandsForVersion('1.21.0')).toHaveLength(1);
|
||||
expect(service.getWorkspaceCommandsForVersion('1.21.0')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should throw on duplicate timestamps within the same kind', async () => {
|
||||
@RegisteredInstanceCommand('1.21.0', 1770000000000)
|
||||
class DuplicateInstanceTimestamp implements MigrationInterface {
|
||||
name = 'DuplicateInstanceTimestamp';
|
||||
|
||||
async up(): Promise<void> {}
|
||||
async down(): Promise<void> {}
|
||||
}
|
||||
|
||||
await expect(
|
||||
buildRegistryService([
|
||||
new MigrationA1770000000000(),
|
||||
new DuplicateInstanceTimestamp(),
|
||||
]),
|
||||
).rejects.toThrow('Duplicate instance command timestamp 1770000000000');
|
||||
});
|
||||
|
||||
it('should throw on duplicate computed names across kinds', async () => {
|
||||
@RegisteredWorkspaceCommand('1.21.0', 1770000000000)
|
||||
class MigrationA1770000000000_WS {
|
||||
async runOnWorkspace(): Promise<void> {}
|
||||
}
|
||||
|
||||
Object.defineProperty(MigrationA1770000000000_WS, 'name', {
|
||||
value: 'MigrationA1770000000000',
|
||||
});
|
||||
|
||||
await expect(
|
||||
buildRegistryService([
|
||||
new MigrationA1770000000000(),
|
||||
new MigrationA1770000000000_WS(),
|
||||
]),
|
||||
).rejects.toThrow(
|
||||
'Duplicate upgrade command name "1.21.0_MigrationA1770000000000_1770000000000"',
|
||||
);
|
||||
});
|
||||
|
||||
it('should return all instance commands across versions sorted by timestamp', async () => {
|
||||
const service = await buildRegistryService([
|
||||
new MigrationC1772000000000(),
|
||||
new MigrationD1769000000000(),
|
||||
new MigrationA1770000000000(),
|
||||
new MigrationB1771000000000(),
|
||||
]);
|
||||
|
||||
const allCommands = service.getAllInstanceCommands();
|
||||
|
||||
expect(allCommands).toStrictEqual([
|
||||
{
|
||||
version: '1.20.0',
|
||||
migration: expect.objectContaining({ name: 'MigrationD1769000000000' }),
|
||||
},
|
||||
{
|
||||
version: '1.21.0',
|
||||
migration: expect.objectContaining({ name: 'MigrationA1770000000000' }),
|
||||
},
|
||||
{
|
||||
version: '1.21.0',
|
||||
migration: expect.objectContaining({ name: 'MigrationB1771000000000' }),
|
||||
},
|
||||
{
|
||||
version: '1.21.0',
|
||||
migration: expect.objectContaining({ name: 'MigrationC1772000000000' }),
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return empty array from getAllInstanceCommands when no commands registered', async () => {
|
||||
const service = await buildRegistryService([]);
|
||||
|
||||
expect(service.getAllInstanceCommands()).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it('should allow same class name with different timestamps across kinds', async () => {
|
||||
@RegisteredWorkspaceCommand('1.21.0', 1790000000000)
|
||||
class MigrationA1770000000000_WS {
|
||||
async runOnWorkspace(): Promise<void> {}
|
||||
}
|
||||
|
||||
Object.defineProperty(MigrationA1770000000000_WS, 'name', {
|
||||
value: 'MigrationA1770000000000',
|
||||
});
|
||||
|
||||
const service = await buildRegistryService([
|
||||
new MigrationA1770000000000(),
|
||||
new MigrationA1770000000000_WS(),
|
||||
]);
|
||||
|
||||
expect(service.getInstanceCommandsForVersion('1.21.0')).toHaveLength(1);
|
||||
expect(service.getWorkspaceCommandsForVersion('1.21.0')).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
-100
@@ -1,100 +0,0 @@
|
||||
import { Injectable, Logger, type OnModuleInit } from '@nestjs/common';
|
||||
import { DiscoveryService } from '@nestjs/core';
|
||||
|
||||
import { type MigrationInterface } from 'typeorm';
|
||||
|
||||
import { getRegisteredInstanceMigrationMetadata } from 'src/database/typeorm/core/decorators/registered-instance-migration.decorator';
|
||||
import {
|
||||
UPGRADE_COMMAND_SUPPORTED_VERSIONS,
|
||||
type UpgradeCommandVersion,
|
||||
} from 'src/engine/constants/upgrade-command-supported-versions.constant';
|
||||
|
||||
type TimestampedMigration = {
|
||||
migration: MigrationInterface;
|
||||
timestamp: number;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class RegisteredInstanceMigrationService implements OnModuleInit {
|
||||
private readonly logger = new Logger(RegisteredInstanceMigrationService.name);
|
||||
|
||||
private readonly migrationsByVersion = new Map<
|
||||
UpgradeCommandVersion,
|
||||
TimestampedMigration[]
|
||||
>();
|
||||
|
||||
constructor(private readonly discoveryService: DiscoveryService) {}
|
||||
|
||||
onModuleInit(): void {
|
||||
for (const version of UPGRADE_COMMAND_SUPPORTED_VERSIONS) {
|
||||
this.migrationsByVersion.set(version, []);
|
||||
}
|
||||
|
||||
const providers = this.discoveryService.getProviders();
|
||||
|
||||
for (const wrapper of providers) {
|
||||
const { instance, metatype } = wrapper;
|
||||
|
||||
if (!instance || !metatype) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const metadata = getRegisteredInstanceMigrationMetadata(metatype);
|
||||
|
||||
if (metadata === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const bucket = this.migrationsByVersion.get(metadata.version);
|
||||
|
||||
if (!bucket) {
|
||||
continue;
|
||||
}
|
||||
|
||||
bucket.push({
|
||||
migration: instance as MigrationInterface,
|
||||
timestamp: metadata.timestamp,
|
||||
});
|
||||
}
|
||||
|
||||
for (const [, bucket] of this.migrationsByVersion) {
|
||||
bucket.sort((entryA, entryB) => entryA.timestamp - entryB.timestamp);
|
||||
}
|
||||
|
||||
for (const [version, bucket] of this.migrationsByVersion) {
|
||||
if (bucket.length > 0) {
|
||||
this.logger.log(
|
||||
`Registered ${bucket.length} versioned migration(s) for ${version}: ${bucket.map((entry) => entry.migration.constructor.name).join(', ')}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getInstanceCommandsForVersion(
|
||||
version: UpgradeCommandVersion,
|
||||
): MigrationInterface[] {
|
||||
return (this.migrationsByVersion.get(version) ?? []).map(
|
||||
(entry) => entry.migration,
|
||||
);
|
||||
}
|
||||
|
||||
getAllInstanceCommands(): {
|
||||
version: UpgradeCommandVersion;
|
||||
migration: MigrationInterface;
|
||||
}[] {
|
||||
const result: {
|
||||
version: UpgradeCommandVersion;
|
||||
migration: MigrationInterface;
|
||||
}[] = [];
|
||||
|
||||
for (const version of UPGRADE_COMMAND_SUPPORTED_VERSIONS) {
|
||||
const bucket = this.migrationsByVersion.get(version) ?? [];
|
||||
|
||||
for (const entry of bucket) {
|
||||
result.push({ version, migration: entry.migration });
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
+228
@@ -0,0 +1,228 @@
|
||||
import { Injectable, Logger, type OnModuleInit } from '@nestjs/common';
|
||||
import { DiscoveryService } from '@nestjs/core';
|
||||
|
||||
import { type MigrationInterface } from 'typeorm';
|
||||
|
||||
import { type ActiveOrSuspendedWorkspaceCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspace.command-runner';
|
||||
import { type WorkspaceCommandRunner } from 'src/database/commands/command-runners/workspace.command-runner';
|
||||
import { getRegisteredWorkspaceCommandMetadata } from 'src/engine/core-modules/upgrade/decorators/registered-workspace-command.decorator';
|
||||
import { getRegisteredInstanceCommandMetadata } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator';
|
||||
import {
|
||||
UPGRADE_COMMAND_SUPPORTED_VERSIONS,
|
||||
type UpgradeCommandVersion,
|
||||
} from 'src/engine/constants/upgrade-command-supported-versions.constant';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type WorkspaceCommand =
|
||||
| WorkspaceCommandRunner
|
||||
| ActiveOrSuspendedWorkspaceCommandRunner;
|
||||
|
||||
type RegisteredInstanceCommand = {
|
||||
name: string;
|
||||
command: MigrationInterface;
|
||||
timestamp: number;
|
||||
};
|
||||
|
||||
type RegisteredWorkspaceCommand = {
|
||||
name: string;
|
||||
command: WorkspaceCommand;
|
||||
timestamp: number;
|
||||
};
|
||||
|
||||
type VersionBucket = {
|
||||
instanceCommands: RegisteredInstanceCommand[];
|
||||
workspaceCommands: RegisteredWorkspaceCommand[];
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class UpgradeCommandRegistryService implements OnModuleInit {
|
||||
private readonly logger = new Logger(UpgradeCommandRegistryService.name);
|
||||
|
||||
private readonly bucketsByVersion = new Map<
|
||||
UpgradeCommandVersion,
|
||||
VersionBucket
|
||||
>();
|
||||
|
||||
constructor(private readonly discoveryService: DiscoveryService) {}
|
||||
|
||||
onModuleInit(): void {
|
||||
for (const version of UPGRADE_COMMAND_SUPPORTED_VERSIONS) {
|
||||
this.bucketsByVersion.set(version, {
|
||||
instanceCommands: [],
|
||||
workspaceCommands: [],
|
||||
});
|
||||
}
|
||||
|
||||
const providers = this.discoveryService.getProviders();
|
||||
|
||||
for (const wrapper of providers) {
|
||||
const { instance, metatype } = wrapper;
|
||||
|
||||
if (!instance || !metatype) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const instanceCommandMetadata =
|
||||
getRegisteredInstanceCommandMetadata(metatype);
|
||||
|
||||
if (isDefined(instanceCommandMetadata)) {
|
||||
const bucket = this.bucketsByVersion.get(
|
||||
instanceCommandMetadata.version,
|
||||
);
|
||||
|
||||
if (isDefined(bucket)) {
|
||||
bucket.instanceCommands.push({
|
||||
name: this.computeCommandName(
|
||||
instanceCommandMetadata.version,
|
||||
(instance as MigrationInterface).constructor.name,
|
||||
instanceCommandMetadata.timestamp,
|
||||
),
|
||||
command: instance as MigrationInterface,
|
||||
timestamp: instanceCommandMetadata.timestamp,
|
||||
});
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
const workspaceCommandMetadata =
|
||||
getRegisteredWorkspaceCommandMetadata(metatype);
|
||||
|
||||
if (isDefined(workspaceCommandMetadata)) {
|
||||
const bucket = this.bucketsByVersion.get(
|
||||
workspaceCommandMetadata.version,
|
||||
);
|
||||
|
||||
if (isDefined(bucket)) {
|
||||
bucket.workspaceCommands.push({
|
||||
name: this.computeCommandName(
|
||||
workspaceCommandMetadata.version,
|
||||
(instance as WorkspaceCommand).constructor.name,
|
||||
workspaceCommandMetadata.timestamp,
|
||||
),
|
||||
command: instance as WorkspaceCommand,
|
||||
timestamp: workspaceCommandMetadata.timestamp,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const [, bucket] of this.bucketsByVersion) {
|
||||
bucket.instanceCommands.sort(
|
||||
(entryA, entryB) => entryA.timestamp - entryB.timestamp,
|
||||
);
|
||||
bucket.workspaceCommands.sort(
|
||||
(entryA, entryB) => entryA.timestamp - entryB.timestamp,
|
||||
);
|
||||
}
|
||||
|
||||
this.validateNoDuplicates();
|
||||
|
||||
for (const [version, bucket] of this.bucketsByVersion) {
|
||||
const totalCount =
|
||||
bucket.instanceCommands.length + bucket.workspaceCommands.length;
|
||||
|
||||
if (totalCount > 0) {
|
||||
this.logger.log(
|
||||
`Registered ${bucket.instanceCommands.length} instance command(s) and ${bucket.workspaceCommands.length} workspace command(s) for ${version}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getInstanceCommandsForVersion(
|
||||
version: UpgradeCommandVersion,
|
||||
): MigrationInterface[] {
|
||||
return (
|
||||
this.bucketsByVersion
|
||||
.get(version)
|
||||
?.instanceCommands.map((entry) => entry.command) ?? []
|
||||
);
|
||||
}
|
||||
|
||||
getWorkspaceCommandsForVersion(
|
||||
version: UpgradeCommandVersion,
|
||||
): WorkspaceCommand[] {
|
||||
return (
|
||||
this.bucketsByVersion
|
||||
.get(version)
|
||||
?.workspaceCommands.map((entry) => entry.command) ?? []
|
||||
);
|
||||
}
|
||||
|
||||
getAllInstanceCommands(): {
|
||||
version: UpgradeCommandVersion;
|
||||
migration: MigrationInterface;
|
||||
}[] {
|
||||
const result: {
|
||||
version: UpgradeCommandVersion;
|
||||
migration: MigrationInterface;
|
||||
}[] = [];
|
||||
|
||||
for (const version of UPGRADE_COMMAND_SUPPORTED_VERSIONS) {
|
||||
for (const command of this.getInstanceCommandsForVersion(version)) {
|
||||
result.push({ version, migration: command });
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private computeCommandName(
|
||||
version: UpgradeCommandVersion,
|
||||
className: string,
|
||||
timestamp: number,
|
||||
): string {
|
||||
return `${version}_${className}_${timestamp}`;
|
||||
}
|
||||
|
||||
private validateNoDuplicates(): void {
|
||||
for (const [version, bucket] of this.bucketsByVersion) {
|
||||
this.validateNoTimestampDuplicatesWithinKind(
|
||||
version,
|
||||
'instance',
|
||||
bucket.instanceCommands,
|
||||
);
|
||||
this.validateNoTimestampDuplicatesWithinKind(
|
||||
version,
|
||||
'workspace',
|
||||
bucket.workspaceCommands,
|
||||
);
|
||||
|
||||
const seenNames = new Set<string>();
|
||||
|
||||
const allNames = [
|
||||
...bucket.instanceCommands.map((entry) => entry.name),
|
||||
...bucket.workspaceCommands.map((entry) => entry.name),
|
||||
];
|
||||
|
||||
for (const name of allNames) {
|
||||
if (seenNames.has(name)) {
|
||||
throw new Error(
|
||||
`Duplicate upgrade command name "${name}" in version ${version}`,
|
||||
);
|
||||
}
|
||||
|
||||
seenNames.add(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private validateNoTimestampDuplicatesWithinKind(
|
||||
version: UpgradeCommandVersion,
|
||||
kind: 'instance' | 'workspace',
|
||||
entries: RegisteredInstanceCommand[] | RegisteredWorkspaceCommand[],
|
||||
): void {
|
||||
const seenTimestamps = new Set<number>();
|
||||
|
||||
for (const entry of entries) {
|
||||
if (seenTimestamps.has(entry.timestamp)) {
|
||||
throw new Error(
|
||||
`Duplicate ${kind} command timestamp ${entry.timestamp} in version ${version} (command: ${entry.name})`,
|
||||
);
|
||||
}
|
||||
|
||||
seenTimestamps.add(entry.timestamp);
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -5,11 +5,11 @@ import { SemVer } from 'semver';
|
||||
import { assertUnreachable, isDefined } from 'twenty-shared/utils';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { type WorkspaceIteratorContext } from 'src/database/commands/command-runners/workspace-iterator.service';
|
||||
import {
|
||||
type UpgradeCommandOptions,
|
||||
type VersionCommands,
|
||||
} from 'src/database/commands/command-runners/upgrade.command-runner';
|
||||
import { type WorkspaceIteratorContext } from 'src/database/commands/command-runners/workspace-iterator.service';
|
||||
} from 'src/database/commands/upgrade-version-command/upgrade.command';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { UpgradeMigrationService } from 'src/engine/core-modules/upgrade/services/upgrade-migration.service';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
|
||||
@@ -3,7 +3,7 @@ import { DiscoveryModule } from '@nestjs/core';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { InstanceUpgradeService } from 'src/engine/core-modules/upgrade/services/instance-upgrade.service';
|
||||
import { RegisteredInstanceMigrationService } from 'src/engine/core-modules/upgrade/services/registered-instance-migration-registry.service';
|
||||
import { UpgradeCommandRegistryService } from 'src/engine/core-modules/upgrade/services/upgrade-command-registry.service';
|
||||
import { UpgradeMigrationService } from 'src/engine/core-modules/upgrade/services/upgrade-migration.service';
|
||||
import { WorkspaceUpgradeService } from 'src/engine/core-modules/upgrade/services/workspace-upgrade.service';
|
||||
import { UpgradeMigrationEntity } from 'src/engine/core-modules/upgrade/upgrade-migration.entity';
|
||||
@@ -18,13 +18,13 @@ import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.ent
|
||||
UpgradeMigrationService,
|
||||
InstanceUpgradeService,
|
||||
WorkspaceUpgradeService,
|
||||
RegisteredInstanceMigrationService,
|
||||
UpgradeCommandRegistryService,
|
||||
],
|
||||
exports: [
|
||||
UpgradeMigrationService,
|
||||
InstanceUpgradeService,
|
||||
WorkspaceUpgradeService,
|
||||
RegisteredInstanceMigrationService,
|
||||
UpgradeCommandRegistryService,
|
||||
],
|
||||
})
|
||||
export class UpgradeModule {}
|
||||
|
||||
Reference in New Issue
Block a user