diff --git a/packages/twenty-oxlint-rules/oxlint-plugin.ts b/packages/twenty-oxlint-rules/oxlint-plugin.ts index 973f9cf3f8..83c550534a 100644 --- a/packages/twenty-oxlint-rules/oxlint-plugin.ts +++ b/packages/twenty-oxlint-rules/oxlint-plugin.ts @@ -64,6 +64,10 @@ import { rule as styledComponentsPrefixedWithStyled, RULE_NAME as styledComponentsPrefixedWithStyledName, } from './rules/styled-components-prefixed-with-styled'; +import { + rule as upgradeCommandFilename, + RULE_NAME as upgradeCommandFilenameName, +} from './rules/upgrade-command-filename'; export default definePlugin({ meta: { name: 'twenty' }, @@ -85,5 +89,6 @@ export default definePlugin({ [sortCssPropertiesAlphabeticallyName]: sortCssPropertiesAlphabetically, [styledComponentsPrefixedWithStyledName]: styledComponentsPrefixedWithStyled, + [upgradeCommandFilenameName]: upgradeCommandFilename, }, }); diff --git a/packages/twenty-oxlint-rules/rules/upgrade-command-filename.spec.ts b/packages/twenty-oxlint-rules/rules/upgrade-command-filename.spec.ts new file mode 100644 index 0000000000..6105752285 --- /dev/null +++ b/packages/twenty-oxlint-rules/rules/upgrade-command-filename.spec.ts @@ -0,0 +1,88 @@ +import { RuleTester } from 'oxlint/plugins-dev'; + +import { rule, RULE_NAME } from './upgrade-command-filename'; + +const ruleTester = new RuleTester(); + +const DUMMY_CODE = 'const x = 1;'; +const BASE = + '/project/packages/twenty-server/src/database/commands/upgrade-version-command'; + +const filename = (path: string) => `${BASE}/${path}`; + +ruleTester.run(RULE_NAME, rule, { + valid: [ + { + code: DUMMY_CODE, + filename: filename( + '1-21/1-21-workspace-command-1775500001000-add-compose-email.command.ts', + ), + }, + { + code: DUMMY_CODE, + filename: filename( + '1-21/1-21-workspace-command-1775500011000-migrate-message-folder-parent-id-to-external-id.command.ts', + ), + }, + { + code: DUMMY_CODE, + filename: filename( + '1-21/1-21-instance-command-fast-1775500000000-add-foo-column.ts', + ), + }, + { + code: DUMMY_CODE, + filename: filename( + '1-22/1-22-instance-command-fast-1780000000000-create-task-table.ts', + ), + }, + { + code: DUMMY_CODE, + filename: filename('1-21/1-21-upgrade-version-command.module.ts'), + }, + { + code: DUMMY_CODE, + filename: filename( + '1-21/__tests__/1-21-workspace-command.spec.ts', + ), + }, + { + code: DUMMY_CODE, + filename: '/project/packages/twenty-front/src/components/Button.tsx', + }, + { + code: DUMMY_CODE, + filename: filename('upgrade.command.ts'), + }, + ], + invalid: [ + { + code: DUMMY_CODE, + filename: filename( + '1-21/1-21-workspace-command-add-feature.command.ts', + ), + errors: [{ messageId: 'invalidWorkspaceCommandFilename' }], + }, + { + code: DUMMY_CODE, + filename: filename( + '1-21/1-21-workspace-command-12345-short-timestamp.command.ts', + ), + errors: [{ messageId: 'invalidWorkspaceCommandFilename' }], + }, + { + code: DUMMY_CODE, + filename: filename( + '1-21/1-21-instance-command-fast-add-column.ts', + ), + errors: [{ messageId: 'invalidInstanceCommandFilename' }], + }, + { + code: DUMMY_CODE, + filename: filename( + '1-21/1-21-add-compose-email-command-menu-item.command.ts', + ), + errors: [{ messageId: 'invalidUpgradeCommandFilename' }], + }, + ], +}); diff --git a/packages/twenty-oxlint-rules/rules/upgrade-command-filename.ts b/packages/twenty-oxlint-rules/rules/upgrade-command-filename.ts new file mode 100644 index 0000000000..09336c052f --- /dev/null +++ b/packages/twenty-oxlint-rules/rules/upgrade-command-filename.ts @@ -0,0 +1,100 @@ +import { defineRule } from '@oxlint/plugins'; + +export const RULE_NAME = 'upgrade-command-filename'; + +const WORKSPACE_COMMAND_REGEX = + /^\d+-\d+-workspace-command-\d{13,}-[a-z0-9]+(?:-[a-z0-9]+)*\.command\.ts$/; + +const INSTANCE_COMMAND_REGEX = + /^\d+-\d+-instance-command-fast-\d{13,}-[a-z0-9]+(?:-[a-z0-9]+)*\.ts$/; + +const SKIPPED_FILE_REGEX = + /\.(module|spec|test|snap)\.ts$|__tests__|__mocks__|__snapshots__/; + +const COMMAND_PREFIX_REGEX = /^\d+-\d+-/; + +const extractUpgradeCommandRelativePath = ( + filename: string, +): string | null => { + const marker = 'upgrade-version-command/'; + const index = filename.indexOf(marker); + + if (index === -1) { + return null; + } + + return filename.slice(index + marker.length); +}; + +export const rule = defineRule({ + meta: { + type: 'problem', + docs: { + description: + 'Enforce consistent filename patterns for workspace and instance upgrade commands', + }, + schema: [], + messages: { + invalidWorkspaceCommandFilename: + "Workspace command filename '{{ name }}' must match pattern: {major}-{minor}-workspace-command-{timestamp}-{description}.command.ts (e.g. '1-21-workspace-command-1775500001000-add-feature.command.ts')", + invalidInstanceCommandFilename: + "Instance command filename '{{ name }}' must match pattern: {major}-{minor}-instance-command-fast-{timestamp}-{description}.ts (e.g. '1-21-instance-command-fast-1775500001000-add-column.ts')", + invalidUpgradeCommandFilename: + "Upgrade command filename '{{ name }}' does not match any recognized pattern. Expected workspace-command or instance-command-fast format.", + }, + }, + create: (context) => { + return { + Program: (node: any) => { + const relativePath = extractUpgradeCommandRelativePath( + context.filename, + ); + + if (!relativePath) { + return; + } + + const segments = relativePath.split('/'); + const basename = segments[segments.length - 1]; + + if (SKIPPED_FILE_REGEX.test(basename)) { + return; + } + + if (!COMMAND_PREFIX_REGEX.test(basename)) { + return; + } + + if (basename.includes('workspace-command-')) { + if (!WORKSPACE_COMMAND_REGEX.test(basename)) { + context.report({ + node, + messageId: 'invalidWorkspaceCommandFilename', + data: { name: basename }, + }); + } + + return; + } + + if (basename.includes('instance-command-fast-')) { + if (!INSTANCE_COMMAND_REGEX.test(basename)) { + context.report({ + node, + messageId: 'invalidInstanceCommandFilename', + data: { name: basename }, + }); + } + + return; + } + + context.report({ + node, + messageId: 'invalidUpgradeCommandFilename', + data: { name: basename }, + }); + }, + }; + }, +}); diff --git a/packages/twenty-server/.oxlintrc.json b/packages/twenty-server/.oxlintrc.json index 5019dfd2c5..fff609e8ee 100644 --- a/packages/twenty-server/.oxlintrc.json +++ b/packages/twenty-server/.oxlintrc.json @@ -60,6 +60,7 @@ "twenty/inject-workspace-repository": "warn", "twenty/rest-api-methods-should-be-guarded": "error", "twenty/graphql-resolvers-should-be-guarded": "error", + "twenty/upgrade-command-filename": "error", "twenty/enforce-module-boundaries": ["error", { "depConstraints": [ { diff --git a/packages/twenty-server/src/database/commands/__tests__/__snapshots__/instance-command-generation.service.spec.ts.snap b/packages/twenty-server/src/database/commands/__tests__/__snapshots__/instance-command-generation.service.spec.ts.snap index ab9a4b2551..3203ff7d6c 100644 --- a/packages/twenty-server/src/database/commands/__tests__/__snapshots__/instance-command-generation.service.spec.ts.snap +++ b/packages/twenty-server/src/database/commands/__tests__/__snapshots__/instance-command-generation.service.spec.ts.snap @@ -2,16 +2,14 @@ exports[`InstanceCommandGenerationService should encode version correctly in file and class names 1`] = ` { - "className": "V1_19_Test_1775000000000", - "fileName": "1-19-instance-command-fast-1775000000000-test.ts", + "className": "TestCommand", + "fileName": "1-20-instance-command-fast-1775000000000-test.ts", "fileTemplate": "import { MigrationInterface, QueryRunner } from 'typeorm'; -import { RegisteredInstanceMigration } from 'src/database/typeorm/core/decorators/registered-instance-migration.decorator'; - -@RegisteredInstanceMigration('1.19.0', 1775000000000) -export class V1_19_Test_1775000000000 implements MigrationInterface { - name = 'V1_19_Test_1775000000000'; +import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator'; +@RegisteredInstanceCommand('1.20.0', 1775000000000) +export class TestCommand implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { await queryRunner.query('SELECT 1'); } @@ -26,16 +24,14 @@ export class V1_19_Test_1775000000000 implements MigrationInterface { exports[`InstanceCommandGenerationService should escape backslashes in SQL queries 1`] = ` { - "className": "V1_21_UpdatePath_1775000000000", + "className": "UpdatePathCommand", "fileName": "1-21-instance-command-fast-1775000000000-update-path.ts", "fileTemplate": "import { MigrationInterface, QueryRunner } from 'typeorm'; -import { RegisteredInstanceMigration } from 'src/database/typeorm/core/decorators/registered-instance-migration.decorator'; - -@RegisteredInstanceMigration('1.21.0', 1775000000000) -export class V1_21_UpdatePath_1775000000000 implements MigrationInterface { - name = 'V1_21_UpdatePath_1775000000000'; +import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator'; +@RegisteredInstanceCommand('1.21.0', 1775000000000) +export class UpdatePathCommand implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { await queryRunner.query('UPDATE "core"."config" SET "value" = E\\'path\\\\\\\\to\\\\\\\\file\\''); } @@ -50,16 +46,14 @@ export class V1_21_UpdatePath_1775000000000 implements MigrationInterface { exports[`InstanceCommandGenerationService should escape single quotes in SQL queries 1`] = ` { - "className": "V1_21_UpdateConfig_1775000000000", + "className": "UpdateConfigCommand", "fileName": "1-21-instance-command-fast-1775000000000-update-config.ts", "fileTemplate": "import { MigrationInterface, QueryRunner } from 'typeorm'; -import { RegisteredInstanceMigration } from 'src/database/typeorm/core/decorators/registered-instance-migration.decorator'; - -@RegisteredInstanceMigration('1.21.0', 1775000000000) -export class V1_21_UpdateConfig_1775000000000 implements MigrationInterface { - name = 'V1_21_UpdateConfig_1775000000000'; +import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator'; +@RegisteredInstanceCommand('1.21.0', 1775000000000) +export class UpdateConfigCommand implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { await queryRunner.query('UPDATE "core"."config" SET "value" = \\'it\\'\\'s done\\''); } @@ -74,16 +68,14 @@ export class V1_21_UpdateConfig_1775000000000 implements MigrationInterface { exports[`InstanceCommandGenerationService should generate a migration with a single up/down query 1`] = ` { - "className": "V1_21_AddFooColumn_1775000000000", + "className": "AddFooColumnCommand", "fileName": "1-21-instance-command-fast-1775000000000-add-foo-column.ts", "fileTemplate": "import { MigrationInterface, QueryRunner } from 'typeorm'; -import { RegisteredInstanceMigration } from 'src/database/typeorm/core/decorators/registered-instance-migration.decorator'; - -@RegisteredInstanceMigration('1.21.0', 1775000000000) -export class V1_21_AddFooColumn_1775000000000 implements MigrationInterface { - name = 'V1_21_AddFooColumn_1775000000000'; +import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator'; +@RegisteredInstanceCommand('1.21.0', 1775000000000) +export class AddFooColumnCommand implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { await queryRunner.query('ALTER TABLE "core"."user" ADD "foo" varchar'); } @@ -98,16 +90,14 @@ export class V1_21_AddFooColumn_1775000000000 implements MigrationInterface { exports[`InstanceCommandGenerationService should generate a migration with multiple queries 1`] = ` { - "className": "V1_21_CreateTaskTable_1775000000000", + "className": "CreateTaskTableCommand", "fileName": "1-21-instance-command-fast-1775000000000-create-task-table.ts", "fileTemplate": "import { MigrationInterface, QueryRunner } from 'typeorm'; -import { RegisteredInstanceMigration } from 'src/database/typeorm/core/decorators/registered-instance-migration.decorator'; - -@RegisteredInstanceMigration('1.21.0', 1775000000000) -export class V1_21_CreateTaskTable_1775000000000 implements MigrationInterface { - name = 'V1_21_CreateTaskTable_1775000000000'; +import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator'; +@RegisteredInstanceCommand('1.21.0', 1775000000000) +export class CreateTaskTableCommand implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { await queryRunner.query('CREATE TABLE "core"."task" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" varchar NOT NULL)'); await queryRunner.query('ALTER TABLE "core"."task" ADD CONSTRAINT "PK_task" PRIMARY KEY ("id")'); @@ -124,16 +114,14 @@ export class V1_21_CreateTaskTable_1775000000000 implements MigrationInterface { exports[`InstanceCommandGenerationService should generate a migration with query parameters 1`] = ` { - "className": "V1_21_SeedSetting_1775000000000", + "className": "SeedSettingCommand", "fileName": "1-21-instance-command-fast-1775000000000-seed-setting.ts", "fileTemplate": "import { MigrationInterface, QueryRunner } from 'typeorm'; -import { RegisteredInstanceMigration } from 'src/database/typeorm/core/decorators/registered-instance-migration.decorator'; - -@RegisteredInstanceMigration('1.21.0', 1775000000000) -export class V1_21_SeedSetting_1775000000000 implements MigrationInterface { - name = 'V1_21_SeedSetting_1775000000000'; +import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator'; +@RegisteredInstanceCommand('1.21.0', 1775000000000) +export class SeedSettingCommand implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { await queryRunner.query('INSERT INTO "core"."setting" ("key", "value") VALUES ($1, $2)', ["theme","dark"]); } @@ -148,16 +136,14 @@ export class V1_21_SeedSetting_1775000000000 implements MigrationInterface { exports[`InstanceCommandGenerationService should use default migration name in class and file names 1`] = ` { - "className": "V1_21_AutoGenerated_1775000000000", + "className": "AutoGeneratedCommand", "fileName": "1-21-instance-command-fast-1775000000000-auto-generated.ts", "fileTemplate": "import { MigrationInterface, QueryRunner } from 'typeorm'; -import { RegisteredInstanceMigration } from 'src/database/typeorm/core/decorators/registered-instance-migration.decorator'; - -@RegisteredInstanceMigration('1.21.0', 1775000000000) -export class V1_21_AutoGenerated_1775000000000 implements MigrationInterface { - name = 'V1_21_AutoGenerated_1775000000000'; +import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator'; +@RegisteredInstanceCommand('1.21.0', 1775000000000) +export class AutoGeneratedCommand implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { await queryRunner.query('ALTER TABLE "core"."user" ADD "bar" integer'); } diff --git a/packages/twenty-server/src/database/commands/__tests__/instance-command-generation.service.spec.ts b/packages/twenty-server/src/database/commands/__tests__/instance-command-generation.service.spec.ts index f556e6372c..1f09ce246f 100644 --- a/packages/twenty-server/src/database/commands/__tests__/instance-command-generation.service.spec.ts +++ b/packages/twenty-server/src/database/commands/__tests__/instance-command-generation.service.spec.ts @@ -171,7 +171,7 @@ describe('InstanceCommandGenerationService', () => { const result = await service.generate({ migrationName: 'test', - version: '1.19.0', + version: '1.20.0', timestamp: FIXED_TIMESTAMP, }); diff --git a/packages/twenty-server/src/database/commands/command-runners/upgrade.command-runner.ts b/packages/twenty-server/src/database/commands/command-runners/upgrade.command-runner.ts deleted file mode 100644 index 44572d585f..0000000000 --- a/packages/twenty-server/src/database/commands/command-runners/upgrade.command-runner.ts +++ /dev/null @@ -1,314 +0,0 @@ -import chalk from 'chalk'; -import { CommandRunner, Option } from 'nest-commander'; -import { SemVer } from 'semver'; -import { assertUnreachable, isDefined } from 'twenty-shared/utils'; -import { DataSource, MigrationInterface } from 'typeorm'; - -import { ActiveOrSuspendedWorkspaceCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspace.command-runner'; -import { WorkspaceIteratorService } from 'src/database/commands/command-runners/workspace-iterator.service'; -import { WorkspaceCommandRunner } from 'src/database/commands/command-runners/workspace.command-runner'; -import { CommandLogger } from 'src/database/commands/logger'; -import { type UpgradeCommandVersion } from 'src/engine/constants/upgrade-command-supported-versions.constant'; -import { CoreEngineVersionService } from 'src/engine/core-engine-version/services/core-engine-version.service'; -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 { WorkspaceUpgradeService } from 'src/engine/core-modules/upgrade/services/workspace-upgrade.service'; -import { WorkspaceVersionService } from 'src/engine/workspace-manager/workspace-version/services/workspace-version.service'; - -export type VersionCommands = ( - | WorkspaceCommandRunner - | ActiveOrSuspendedWorkspaceCommandRunner -)[]; -export type AllCommands = Record; - -export type UpgradeCommandOptions = { - workspaceId?: Set; - startFromWorkspaceId?: string; - workspaceCountLimit?: number; - dryRun?: boolean; - verbose?: boolean; -}; - -type VersionContext = { - fromWorkspaceVersion: SemVer; - currentAppVersion: SemVer; - currentVersionMajorMinor: UpgradeCommandVersion; - instanceCommands: MigrationInterface[]; - workspaceCommands: VersionCommands; -}; - -export abstract class UpgradeCommandRunner extends CommandRunner { - protected logger: CommandLogger; - - public abstract allCommands: AllCommands; - - constructor( - protected readonly coreEngineVersionService: CoreEngineVersionService, - protected readonly workspaceVersionService: WorkspaceVersionService, - protected readonly registeredInstanceMigrationService: RegisteredInstanceMigrationService, - protected readonly instanceUpgradeService: InstanceUpgradeService, - protected readonly workspaceIteratorService: WorkspaceIteratorService, - protected readonly workspaceUpgradeService: WorkspaceUpgradeService, - protected readonly dataSource: DataSource, - ) { - super(); - this.logger = new CommandLogger({ - verbose: false, - constructorName: this.constructor.name, - }); - } - - @Option({ - flags: '-d, --dry-run', - description: 'Simulate the command without making actual changes', - required: false, - }) - parseDryRun(): boolean { - return true; - } - - @Option({ - flags: '-v, --verbose', - description: 'Verbose output', - required: false, - }) - parseVerbose(): boolean { - return true; - } - - @Option({ - flags: '-w, --workspace-id [workspace_id]', - description: - 'workspace id. Command runs on all active/suspended workspaces if not provided.', - required: false, - }) - parseWorkspaceId(val: string, previous?: Set): Set { - const accumulator = previous ?? new Set(); - - accumulator.add(val); - - return accumulator; - } - - @Option({ - flags: '--start-from-workspace-id [workspace_id]', - description: - 'Start from a specific workspace id. Workspaces are processed in ascending order of id.', - required: false, - }) - parseStartFromWorkspaceId(val: string): string { - return val; - } - - @Option({ - flags: '--workspace-count-limit [count]', - description: - 'Limit the number of workspaces to process. Workspaces are processed in ascending order of id.', - required: false, - }) - parseWorkspaceCountLimit(val: string): number { - const limit = parseInt(val); - - if (isNaN(limit)) { - throw new Error('Workspace count limit must be a number'); - } - - if (limit <= 0) { - throw new Error('Workspace count limit must be greater than 0'); - } - - return limit; - } - - override async run( - _passedParams: string[], - options: UpgradeCommandOptions, - ): Promise { - if (options.verbose) { - this.logger = new CommandLogger({ - verbose: true, - constructorName: this.constructor.name, - }); - } - - try { - const versionContext = this.resolveVersionContext(); - this.logger.log( - chalk.blue( - [ - 'Initialized upgrade context with:', - `- currentVersion (migrating to): ${versionContext.currentAppVersion}`, - `- fromWorkspaceVersion: ${versionContext.fromWorkspaceVersion}`, - `- ${versionContext.instanceCommands.length} instance commands (from registry)`, - `- ${versionContext.workspaceCommands.length} workspace commands`, - ].join('\n '), - ), - ); - - const workspacesBelowMinimumVersion = - await this.workspaceVersionService.getWorkspacesBelowVersion( - versionContext.fromWorkspaceVersion.version, - ); - - if (workspacesBelowMinimumVersion.length > 0) { - const ineligibleIds = workspacesBelowMinimumVersion - .map((workspace) => workspace.id) - .join(', '); - - throw new Error( - `Unable to run the upgrade command. Aborting the upgrade process. -Workspaces below minimum version (${versionContext.fromWorkspaceVersion.version}): ${ineligibleIds}. -Please roll back to that version and run the upgrade command again.`, - ); - } - - await this.runLegacyPendingTypeOrmMigrations(); - await this.runInstanceCommandsOrThrow(versionContext); - - const hasWorkspaces = - await this.workspaceVersionService.hasActiveOrSuspendedWorkspaces(); - - if (!hasWorkspaces) { - this.logger.log( - chalk.blue( - 'Fresh installation detected, skipping workspace commands', - ), - ); - - return; - } - - const iteratorReport = await this.runWorkspaceCommands( - options, - versionContext, - ); - - this.logger.log( - chalk.blue( - `Upgrade summary: ${iteratorReport.success.length} succeeded, ${iteratorReport.fail.length} failed`, - ), - ); - - if (iteratorReport.fail.length > 0) { - throw new Error( - `Upgrade completed with ${iteratorReport.fail.length} workspace failure(s)`, - ); - } - } catch (error) { - this.logger.error(chalk.red(`Upgrade failed: ${error.message}`)); - throw error; - } - } - - private async runLegacyPendingTypeOrmMigrations(): Promise { - this.logger.log('Running legacy TypeORM migrations...'); - - const migrations = await this.dataSource.runMigrations({ - transaction: 'each', - }); - - if (migrations.length === 0) { - this.logger.log('No pending legacy migrations'); - } else { - this.logger.log( - `Executed ${migrations.length} legacy migration(s): ${migrations.map((migration) => migration.name).join(', ')}`, - ); - } - } - - private async runInstanceCommandsOrThrow( - versionContext: VersionContext, - ): Promise { - for (const instanceCommand of versionContext.instanceCommands) { - const migrationName = instanceCommand.constructor.name; - const result = - await this.instanceUpgradeService.runSingleMigration(instanceCommand); - - switch (result.status) { - case 'already-executed': { - this.logger.warn( - `Core migration ${migrationName} already executed, skipping`, - ); - - break; - } - case 'failed': { - this.logger.error(`Core migration ${migrationName} failed`); - - if (isDefined(result.error)) { - this.logger.error( - result.error instanceof Error - ? (result.error.stack ?? result.error.message) - : String(result.error), - ); - } - - throw new Error(`Core migration ${migrationName} failed`); - } - case 'success': { - this.logger.log( - `Core migration ${migrationName} executed successfully`, - ); - - break; - } - default: { - assertUnreachable(result); - } - } - } - } - - private resolveVersionContext(): VersionContext { - const currentAppVersion = this.coreEngineVersionService.getCurrentVersion(); - const currentVersionMajorMinor = - `${currentAppVersion.major}.${currentAppVersion.minor}.0` as UpgradeCommandVersion; - const workspaceCommands = this.allCommands[currentVersionMajorMinor]; - - if (!isDefined(workspaceCommands)) { - throw new Error( - `No command found for version ${currentAppVersion}. Please check the commands record.`, - ); - } - - const fromWorkspaceVersion = - this.coreEngineVersionService.getPreviousVersion(); - - const instanceCommands = - this.registeredInstanceMigrationService.getInstanceCommandsForVersion( - currentVersionMajorMinor, - ); - - return { - fromWorkspaceVersion, - currentAppVersion, - currentVersionMajorMinor, - workspaceCommands, - instanceCommands, - }; - } - - private async runWorkspaceCommands( - options: UpgradeCommandOptions, - versionContext: VersionContext, - ) { - return await this.workspaceIteratorService.iterate({ - workspaceIds: - options.workspaceId && options.workspaceId.size > 0 - ? Array.from(options.workspaceId) - : undefined, - startFromWorkspaceId: options.startFromWorkspaceId, - workspaceCountLimit: options.workspaceCountLimit, - dryRun: options.dryRun, - callback: async (context) => { - await this.workspaceUpgradeService.upgradeWorkspace({ - iteratorContext: context, - options, - fromWorkspaceVersion: versionContext.fromWorkspaceVersion, - currentAppVersion: versionContext.currentAppVersion, - workspaceCommands: versionContext.workspaceCommands, - }); - }, - }); - } -} diff --git a/packages/twenty-server/src/database/commands/generate-instance-command.command.ts b/packages/twenty-server/src/database/commands/generate-instance-command.command.ts index a251087123..9a254ff88b 100644 --- a/packages/twenty-server/src/database/commands/generate-instance-command.command.ts +++ b/packages/twenty-server/src/database/commands/generate-instance-command.command.ts @@ -20,7 +20,7 @@ type GenerateInstanceCommandOptions = { @Command({ name: 'generate:instance-command', description: - 'Generate an instance command with @RegisteredInstanceMigration decorator for the latest supported version', + 'Generate an instance command with @RegisteredInstanceCommand decorator for the latest supported version', }) export class GenerateInstanceCommandCommand extends CommandRunner { private readonly logger = new Logger(GenerateInstanceCommandCommand.name); diff --git a/packages/twenty-server/src/database/commands/instance-command-generation.service.ts b/packages/twenty-server/src/database/commands/instance-command-generation.service.ts index bf56a1348c..c4b3d32d26 100644 --- a/packages/twenty-server/src/database/commands/instance-command-generation.service.ts +++ b/packages/twenty-server/src/database/commands/instance-command-generation.service.ts @@ -38,7 +38,7 @@ export class InstanceCommandGenerationService { return null; } - const className = this.buildClassName(migrationName, version, timestamp); + const className = this.buildClassName(migrationName); const upStatements = sqlInMemory.upQueries.map( ({ query, parameters }) => @@ -66,14 +66,8 @@ export class InstanceCommandGenerationService { return { fileName, fileTemplate, className }; } - private buildClassName( - name: string, - version: string, - timestamp: number, - ): string { - const versionSlug = version.split('.').slice(0, 2).join('_'); - - return `V${versionSlug}_${pascalCase(name)}_${timestamp}`; + private buildClassName(name: string): string { + return `${pascalCase(name)}Command`; } private formatQueryParams(parameters: unknown[] | undefined): string { @@ -103,12 +97,10 @@ export class InstanceCommandGenerationService { }): string { return `import { MigrationInterface, QueryRunner } from 'typeorm'; -import { RegisteredInstanceMigration } from 'src/database/typeorm/core/decorators/registered-instance-migration.decorator'; +import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator'; -@RegisteredInstanceMigration('${version}', ${timestamp}) +@RegisteredInstanceCommand('${version}', ${timestamp}) export class ${className} implements MigrationInterface { - name = '${className}'; - public async up(queryRunner: QueryRunner): Promise { ${upStatements.join('\n')} } diff --git a/packages/twenty-server/src/database/commands/run-instance-commands.command.ts b/packages/twenty-server/src/database/commands/run-instance-commands.command.ts index 9b8c662085..3334cefc00 100644 --- a/packages/twenty-server/src/database/commands/run-instance-commands.command.ts +++ b/packages/twenty-server/src/database/commands/run-instance-commands.command.ts @@ -8,7 +8,7 @@ import { DataSource } from 'typeorm'; import { CoreEngineVersionService } from 'src/engine/core-engine-version/services/core-engine-version.service'; 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 { WorkspaceVersionService } from 'src/engine/workspace-manager/workspace-version/services/workspace-version.service'; type RunInstanceCommandsOptions = { @@ -28,7 +28,7 @@ export class RunInstanceCommandsCommand extends CommandRunner { private readonly dataSource: DataSource, private readonly coreEngineVersionService: CoreEngineVersionService, private readonly workspaceVersionService: WorkspaceVersionService, - private readonly registeredInstanceMigrationService: RegisteredInstanceMigrationService, + private readonly upgradeCommandRegistryService: UpgradeCommandRegistryService, private readonly instanceUpgradeService: InstanceUpgradeService, ) { super(); @@ -79,7 +79,7 @@ export class RunInstanceCommandsCommand extends CommandRunner { private async runAllInstanceCommands(): Promise { const allInstanceCommands = - this.registeredInstanceMigrationService.getAllInstanceCommands(); + this.upgradeCommandRegistryService.getAllInstanceCommands(); if (allInstanceCommands.length === 0) { this.logger.log('No registered instance commands'); diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-backfill-command-menu-items.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-backfill-command-menu-items.command.ts deleted file mode 100644 index 0a6fd31023..0000000000 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-backfill-command-menu-items.command.ts +++ /dev/null @@ -1,379 +0,0 @@ -import { isNonEmptyString } from '@sniptt/guards'; -import { Command } from 'nest-commander'; -import { FeatureFlagKey } from 'twenty-shared/types'; -import { isDefined } from 'twenty-shared/utils'; -import { v4 as uuidv4 } from 'uuid'; - -import { ActiveOrSuspendedWorkspaceCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspace.command-runner'; -import { type RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspace.command-runner'; -import { WorkspaceIteratorService } from 'src/database/commands/command-runners/workspace-iterator.service'; -import { ApplicationService } from 'src/engine/core-modules/application/application.service'; -import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type'; -import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; -import { CommandMenuItemAvailabilityType } from 'src/engine/metadata-modules/command-menu-item/enums/command-menu-item-availability-type.enum'; -import { EngineComponentKey } from 'src/engine/metadata-modules/command-menu-item/enums/engine-component-key.enum'; -import { type FlatCommandMenuItem } from 'src/engine/metadata-modules/flat-command-menu-item/types/flat-command-menu-item.type'; -import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; -import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; -import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; -import { computeTwentyStandardApplicationAllFlatEntityMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/twenty-standard-application-all-flat-entity-maps.constant'; -import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service'; -import { - WorkflowVersionStatus, - type WorkflowVersionWorkspaceEntity, -} from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity'; -import { type WorkflowWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow.workspace-entity'; -import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service'; -import { - type WorkflowManualTrigger, - WorkflowTriggerType, -} from 'src/modules/workflow/workflow-trigger/types/workflow-trigger.type'; - -@Command({ - name: 'upgrade:1-20:backfill-command-menu-items', - description: - 'Backfill missing standard and trigger workflow version command menu items for existing workspaces and enable IS_COMMAND_MENU_ITEM_ENABLED feature flag', -}) -export class BackfillCommandMenuItemsCommand extends ActiveOrSuspendedWorkspaceCommandRunner { - constructor( - private readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager, - private readonly applicationService: ApplicationService, - private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService, - private readonly featureFlagService: FeatureFlagService, - private readonly workspaceCacheService: WorkspaceCacheService, - private readonly workflowCommonWorkspaceService: WorkflowCommonWorkspaceService, - protected readonly workspaceIteratorService: WorkspaceIteratorService, - ) { - super(workspaceIteratorService); - } - - override async runOnWorkspace({ - workspaceId, - options, - }: RunOnWorkspaceArgs): Promise { - const isDryRun = options.dryRun ?? false; - - this.logger.log( - `${isDryRun ? '[DRY RUN] ' : ''}Starting backfill of command menu items for workspace ${workspaceId}`, - ); - - const isFeatureFlagAlreadyEnabled = - await this.featureFlagService.isFeatureEnabled( - FeatureFlagKey.IS_COMMAND_MENU_ITEM_ENABLED, - workspaceId, - ); - - if (isFeatureFlagAlreadyEnabled) { - this.logger.log( - `IS_COMMAND_MENU_ITEM_ENABLED already enabled for workspace ${workspaceId}, skipping`, - ); - - return; - } - - const { twentyStandardFlatApplication, workspaceCustomFlatApplication } = - await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow( - { workspaceId }, - ); - - const standardCommandMenuItems = - await this.computeStandardCommandMenuItemsToCreate( - workspaceId, - twentyStandardFlatApplication, - ); - - const triggerWorkflowVersionCommandMenuItems = - await this.computeTriggerWorkflowVersionCommandMenuItemsToCreate( - workspaceId, - workspaceCustomFlatApplication, - ); - - const totalCount = - standardCommandMenuItems.length + - triggerWorkflowVersionCommandMenuItems.length; - - if (totalCount === 0) { - this.logger.log( - `No missing command menu items for workspace ${workspaceId}`, - ); - - return; - } - - this.logger.log( - `Found ${totalCount} missing command menu item(s) for workspace ${workspaceId} (${standardCommandMenuItems.length} standard, ${triggerWorkflowVersionCommandMenuItems.length} trigger workflow version)`, - ); - - if (isDryRun) { - this.logger.log( - `[DRY RUN] Would create ${totalCount} command menu item(s) for workspace ${workspaceId}`, - ); - - return; - } - - if (standardCommandMenuItems.length > 0) { - await this.createCommandMenuItems({ - workspaceId, - flatCommandMenuItemsToCreate: standardCommandMenuItems, - applicationUniversalIdentifier: - twentyStandardFlatApplication.universalIdentifier, - }); - } - - if (triggerWorkflowVersionCommandMenuItems.length > 0) { - await this.createCommandMenuItems({ - workspaceId, - flatCommandMenuItemsToCreate: triggerWorkflowVersionCommandMenuItems, - applicationUniversalIdentifier: - workspaceCustomFlatApplication.universalIdentifier, - }); - } - - await this.featureFlagService.enableFeatureFlags( - [FeatureFlagKey.IS_COMMAND_MENU_ITEM_ENABLED], - workspaceId, - ); - - this.logger.log( - `Successfully backfilled ${totalCount} command menu item(s) for workspace ${workspaceId}`, - ); - } - - private async computeStandardCommandMenuItemsToCreate( - workspaceId: string, - twentyStandardFlatApplication: FlatApplication, - ): Promise { - const { allFlatEntityMaps: standardAllFlatEntityMaps } = - computeTwentyStandardApplicationAllFlatEntityMaps({ - shouldIncludeRecordPageLayouts: true, - now: new Date().toISOString(), - workspaceId, - twentyStandardApplicationId: twentyStandardFlatApplication.id, - }); - - const { flatCommandMenuItemMaps: existingFlatCommandMenuItemMaps } = - await this.workspaceCacheService.getOrRecompute(workspaceId, [ - 'flatCommandMenuItemMaps', - ]); - - const commandMenuItemsToCreate = Object.values( - standardAllFlatEntityMaps.flatCommandMenuItemMaps.byUniversalIdentifier, - ) - .filter(isDefined) - .filter( - (commandMenuItem) => - !isDefined( - existingFlatCommandMenuItemMaps.byUniversalIdentifier[ - commandMenuItem.universalIdentifier - ], - ), - ); - - if (commandMenuItemsToCreate.length > 0) { - this.logger.log( - `Found ${commandMenuItemsToCreate.length} missing standard command menu item(s) for workspace ${workspaceId}`, - ); - } - - return commandMenuItemsToCreate; - } - - private async computeTriggerWorkflowVersionCommandMenuItemsToCreate( - workspaceId: string, - workspaceCustomFlatApplication: FlatApplication, - ): Promise { - const authContext = buildSystemAuthContext(workspaceId); - - return await this.twentyORMGlobalManager.executeInWorkspaceContext( - async () => { - const workflowVersionRepository = - await this.twentyORMGlobalManager.getRepository( - workspaceId, - 'workflowVersion', - { shouldBypassPermissionChecks: true }, - ); - - const activeWorkflowVersions = await workflowVersionRepository.find({ - where: { status: WorkflowVersionStatus.ACTIVE }, - }); - - const manualTriggerVersions = activeWorkflowVersions.filter( - (version) => - isDefined(version.trigger) && - version.trigger.type === WorkflowTriggerType.MANUAL, - ); - - if (manualTriggerVersions.length === 0) { - this.logger.log( - `No active workflow versions with manual triggers for workspace ${workspaceId}`, - ); - - return []; - } - - const { flatCommandMenuItemMaps: existingFlatCommandMenuItemMaps } = - await this.workspaceCacheService.getOrRecompute(workspaceId, [ - 'flatCommandMenuItemMaps', - ]); - - const existingWorkflowVersionIds = new Set( - Object.values(existingFlatCommandMenuItemMaps.byUniversalIdentifier) - .filter(isDefined) - .map((item) => item.workflowVersionId) - .filter(isDefined), - ); - - const workflowRepository = - await this.twentyORMGlobalManager.getRepository( - workspaceId, - 'workflow', - { shouldBypassPermissionChecks: true }, - ); - - const flatCommandMenuItemsToCreate: FlatCommandMenuItem[] = []; - - for (const workflowVersion of manualTriggerVersions) { - if (existingWorkflowVersionIds.has(workflowVersion.id)) { - continue; - } - - const workflow = await workflowRepository.findOne({ - where: { id: workflowVersion.workflowId }, - }); - - const label = - workflow && isNonEmptyString(workflow.name) - ? workflow.name - : 'Manual Trigger'; - - const trigger = workflowVersion.trigger as WorkflowManualTrigger; - - const { - availabilityType, - availabilityObjectMetadataId, - availabilityObjectMetadataUniversalIdentifier, - } = await this.resolveManualTriggerAvailability(trigger, workspaceId); - - const id = uuidv4(); - const now = new Date().toISOString(); - - flatCommandMenuItemsToCreate.push({ - id, - universalIdentifier: id, - workflowVersionId: workflowVersion.id, - frontComponentId: null, - frontComponentUniversalIdentifier: null, - engineComponentKey: EngineComponentKey.TRIGGER_WORKFLOW_VERSION, - label, - shortLabel: label, - icon: trigger.settings.icon ?? null, - isPinned: trigger.settings.isPinned ?? false, - position: 0, - payload: null, - hotKeys: null, - availabilityType, - availabilityObjectMetadataId: availabilityObjectMetadataId ?? null, - availabilityObjectMetadataUniversalIdentifier, - conditionalAvailabilityExpression: null, - workspaceId, - applicationId: workspaceCustomFlatApplication.id, - applicationUniversalIdentifier: - workspaceCustomFlatApplication.universalIdentifier, - createdAt: now, - updatedAt: now, - }); - } - - if (flatCommandMenuItemsToCreate.length > 0) { - this.logger.log( - `Found ${flatCommandMenuItemsToCreate.length} missing trigger workflow version command menu item(s) for workspace ${workspaceId}`, - ); - } - - return flatCommandMenuItemsToCreate; - }, - authContext, - ); - } - - private async createCommandMenuItems({ - workspaceId, - flatCommandMenuItemsToCreate, - applicationUniversalIdentifier, - }: { - workspaceId: string; - flatCommandMenuItemsToCreate: FlatCommandMenuItem[]; - applicationUniversalIdentifier: string; - }): Promise { - const validateAndBuildResult = - await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( - { - allFlatEntityOperationByMetadataName: { - commandMenuItem: { - flatEntityToCreate: flatCommandMenuItemsToCreate, - flatEntityToDelete: [], - flatEntityToUpdate: [], - }, - }, - workspaceId, - applicationUniversalIdentifier, - }, - ); - - if (validateAndBuildResult.status === 'fail') { - this.logger.error( - `Failed to backfill command menu items:\n${JSON.stringify(validateAndBuildResult, null, 2)}`, - ); - - throw new Error( - `Failed to backfill command menu items for workspace ${workspaceId}`, - ); - } - } - - private async resolveManualTriggerAvailability( - trigger: WorkflowManualTrigger, - workspaceId: string, - ): Promise<{ - availabilityType: CommandMenuItemAvailabilityType; - availabilityObjectMetadataId: string | undefined; - availabilityObjectMetadataUniversalIdentifier: string | null; - }> { - const availability = trigger.settings.availability; - - if (!isDefined(availability) || availability.type === 'GLOBAL') { - return { - availabilityType: CommandMenuItemAvailabilityType.GLOBAL, - availabilityObjectMetadataId: undefined, - availabilityObjectMetadataUniversalIdentifier: null, - }; - } - - const { objectIdByNameSingular, flatObjectMetadataMaps } = - await this.workflowCommonWorkspaceService.getFlatEntityMaps(workspaceId); - - const objectId = objectIdByNameSingular[availability.objectNameSingular]; - - if (!isDefined(objectId)) { - this.logger.warn( - `Object metadata not found for "${availability.objectNameSingular}" in workspace ${workspaceId}, falling back to GLOBAL`, - ); - - return { - availabilityType: CommandMenuItemAvailabilityType.GLOBAL, - availabilityObjectMetadataId: undefined, - availabilityObjectMetadataUniversalIdentifier: null, - }; - } - - const objectUniversalIdentifier = - flatObjectMetadataMaps.universalIdentifierById[objectId] ?? null; - - return { - availabilityType: CommandMenuItemAvailabilityType.RECORD_SELECTION, - availabilityObjectMetadataId: objectId, - availabilityObjectMetadataUniversalIdentifier: objectUniversalIdentifier, - }; - } -} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-backfill-navigation-menu-item-type.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-backfill-navigation-menu-item-type.command.ts deleted file mode 100644 index 7053aaf292..0000000000 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-backfill-navigation-menu-item-type.command.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { InjectDataSource } from '@nestjs/typeorm'; - -import { Command } from 'nest-commander'; -import { DataSource } from 'typeorm'; - -import { ActiveOrSuspendedWorkspaceCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspace.command-runner'; -import { type RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspace.command-runner'; -import { WorkspaceIteratorService } from 'src/database/commands/command-runners/workspace-iterator.service'; -import { makeNavigationMenuItemTypeNotNullQueries } from 'src/database/typeorm/core/migrations/utils/1773681736596-makeNavigationMenuItemTypeNotNull.util'; - -@Command({ - name: 'upgrade:1-20:backfill-navigation-menu-item-type', - description: - 'Backfill navigation menu item type based on existing columns, then apply NOT NULL and CHECK constraints', -}) -export class BackfillNavigationMenuItemTypeCommand extends ActiveOrSuspendedWorkspaceCommandRunner { - private hasRunOnce = false; - - constructor( - protected readonly workspaceIteratorService: WorkspaceIteratorService, - @InjectDataSource() - private readonly coreDataSource: DataSource, - ) { - super(workspaceIteratorService); - } - - override async runOnWorkspace({ - options, - }: RunOnWorkspaceArgs): Promise { - if (this.hasRunOnce) { - this.logger.warn( - 'Skipping has already been run once BackfillNavigationMenuItemTypeCommand', - ); - - return; - } - - if (options.dryRun) { - return; - } - - const queryRunner = this.coreDataSource.createQueryRunner(); - - await queryRunner.connect(); - await queryRunner.startTransaction(); - - try { - await this.backfillType(queryRunner); - await this.cleanConflictingColumns(queryRunner); - await queryRunner.commitTransaction(); - } catch (error) { - await queryRunner.rollbackTransaction(); - this.logger.error( - `Rolling back BackfillNavigationMenuItemTypeCommand data backfill: ${error.message}`, - ); - - await queryRunner.release(); - - return; - } - - await queryRunner.startTransaction(); - - try { - await makeNavigationMenuItemTypeNotNullQueries(queryRunner); - await queryRunner.commitTransaction(); - this.logger.log('Successfully run BackfillNavigationMenuItemTypeCommand'); - this.hasRunOnce = true; - } catch (error) { - await queryRunner.rollbackTransaction(); - this.logger.error( - `Rolling back BackfillNavigationMenuItemTypeCommand schema changes: ${error.message}`, - ); - } finally { - await queryRunner.release(); - } - } - - private async backfillType( - queryRunner: ReturnType, - ): Promise { - await queryRunner.query( - `UPDATE "core"."navigationMenuItem" SET "type" = 'OBJECT' WHERE "type" = 'VIEW' AND "targetObjectMetadataId" IS NOT NULL AND "targetRecordId" IS NULL`, - ); - - await queryRunner.query( - `UPDATE "core"."navigationMenuItem" SET "type" = 'RECORD' WHERE "type" IS NULL AND "targetRecordId" IS NOT NULL AND "targetObjectMetadataId" IS NOT NULL`, - ); - - await queryRunner.query( - `UPDATE "core"."navigationMenuItem" SET "type" = 'OBJECT' WHERE "type" IS NULL AND "targetObjectMetadataId" IS NOT NULL AND "targetRecordId" IS NULL`, - ); - - await queryRunner.query( - `UPDATE "core"."navigationMenuItem" SET "type" = 'VIEW' WHERE "type" IS NULL AND "viewId" IS NOT NULL`, - ); - - await queryRunner.query( - `UPDATE "core"."navigationMenuItem" SET "type" = 'LINK' WHERE "type" IS NULL AND "link" IS NOT NULL`, - ); - - await queryRunner.query( - `UPDATE "core"."navigationMenuItem" SET "type" = 'FOLDER' WHERE "type" IS NULL`, - ); - } - - private async cleanConflictingColumns( - queryRunner: ReturnType, - ): Promise { - await queryRunner.query( - `UPDATE "core"."navigationMenuItem" SET "targetRecordId" = NULL, "targetObjectMetadataId" = NULL, "link" = NULL WHERE "type" = 'VIEW'`, - ); - - await queryRunner.query( - `UPDATE "core"."navigationMenuItem" SET "viewId" = NULL, "link" = NULL WHERE "type" = 'RECORD'`, - ); - - await queryRunner.query( - `UPDATE "core"."navigationMenuItem" SET "viewId" = NULL, "targetRecordId" = NULL, "link" = NULL WHERE "type" = 'OBJECT'`, - ); - - await queryRunner.query( - `UPDATE "core"."navigationMenuItem" SET "viewId" = NULL, "targetRecordId" = NULL, "targetObjectMetadataId" = NULL WHERE "type" = 'LINK'`, - ); - - await queryRunner.query( - `UPDATE "core"."navigationMenuItem" SET "viewId" = NULL, "targetRecordId" = NULL, "targetObjectMetadataId" = NULL, "link" = NULL WHERE "type" = 'FOLDER'`, - ); - } -} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-backfill-select-field-option-ids.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-backfill-select-field-option-ids.command.ts deleted file mode 100644 index 35a6d54478..0000000000 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-backfill-select-field-option-ids.command.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { InjectDataSource } from '@nestjs/typeorm'; - -import { Command } from 'nest-commander'; -import { isDefined } from 'twenty-shared/utils'; -import { v4 } from 'uuid'; -import { DataSource } from 'typeorm'; - -import { ActiveOrSuspendedWorkspaceCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspace.command-runner'; -import { type RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspace.command-runner'; -import { WorkspaceIteratorService } from 'src/database/commands/command-runners/workspace-iterator.service'; - -@Command({ - name: 'upgrade:1-20:backfill-select-field-option-ids', - description: - 'Backfill missing ids on SELECT and MULTI_SELECT field metadata options', -}) -export class BackfillSelectFieldOptionIdsCommand extends ActiveOrSuspendedWorkspaceCommandRunner { - constructor( - protected readonly workspaceIteratorService: WorkspaceIteratorService, - @InjectDataSource() - private readonly coreDataSource: DataSource, - ) { - super(workspaceIteratorService); - } - - override async runOnWorkspace({ - workspaceId, - options, - }: RunOnWorkspaceArgs): Promise { - const dryRun = options?.dryRun ?? false; - - const selectFields: { id: string; options: Record[] }[] = - await this.coreDataSource.query( - `SELECT "id", "options" - FROM core."fieldMetadata" - WHERE "workspaceId" = $1 - AND "type" IN ('SELECT', 'MULTI_SELECT') - AND "options" IS NOT NULL`, - [workspaceId], - ); - - const fieldsToUpdate = selectFields.filter((field) => - field.options.some((option) => !isDefined(option.id)), - ); - - if (fieldsToUpdate.length === 0) { - this.logger.log( - `No SELECT/MULTI_SELECT options missing ids in workspace ${workspaceId}`, - ); - - return; - } - - this.logger.log( - `${dryRun ? '[DRY RUN] ' : ''}Found ${fieldsToUpdate.length} field(s) with options missing ids in workspace ${workspaceId}`, - ); - - if (dryRun) { - return; - } - - const queryRunner = this.coreDataSource.createQueryRunner(); - - await queryRunner.connect(); - await queryRunner.startTransaction(); - - try { - for (const field of fieldsToUpdate) { - const patchedOptions = field.options.map((option) => ({ - ...option, - id: isDefined(option.id) ? option.id : v4(), - })); - - await queryRunner.query( - `UPDATE core."fieldMetadata" - SET "options" = $1::jsonb - WHERE "id" = $2`, - [JSON.stringify(patchedOptions), field.id], - ); - } - - await queryRunner.commitTransaction(); - - this.logger.log( - `Backfilled option ids for ${fieldsToUpdate.length} field(s) in workspace ${workspaceId}`, - ); - } catch (error) { - await queryRunner.rollbackTransaction(); - throw error; - } finally { - await queryRunner.release(); - } - } -} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-delete-orphan-navigation-menu-items.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-delete-orphan-navigation-menu-items.command.ts deleted file mode 100644 index 5551c1e4f8..0000000000 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-delete-orphan-navigation-menu-items.command.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { InjectRepository } from '@nestjs/typeorm'; - -import { Command } from 'nest-commander'; -import { In, Repository } from 'typeorm'; -import { isDefined } from 'twenty-shared/utils'; -import { NavigationMenuItemType } from 'twenty-shared/types'; - -import { ActiveOrSuspendedWorkspaceCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspace.command-runner'; -import { type RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspace.command-runner'; -import { WorkspaceIteratorService } from 'src/database/commands/command-runners/workspace-iterator.service'; -import { NavigationMenuItemEntity } from 'src/engine/metadata-modules/navigation-menu-item/entities/navigation-menu-item.entity'; -import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; - -@Command({ - name: 'upgrade:1-20:delete-orphan-navigation-menu-items', - description: 'Delete navigation menu items pointing to deleted views', -}) -export class DeleteOrphanNavigationMenuItemsCommand extends ActiveOrSuspendedWorkspaceCommandRunner { - constructor( - protected readonly workspaceIteratorService: WorkspaceIteratorService, - @InjectRepository(NavigationMenuItemEntity) - private readonly navigationMenuItemRepository: Repository, - private readonly workspaceCacheService: WorkspaceCacheService, - ) { - super(workspaceIteratorService); - } - - override async runOnWorkspace({ - workspaceId, - options, - }: RunOnWorkspaceArgs): Promise { - const { flatViewMaps, flatNavigationMenuItemMaps } = - await this.workspaceCacheService.getOrRecompute(workspaceId, [ - 'flatViewMaps', - 'flatNavigationMenuItemMaps', - ]); - - const activeViewIds = new Set( - Object.values(flatViewMaps.byUniversalIdentifier) - .filter((view): view is NonNullable => isDefined(view)) - .filter((view) => view.deletedAt === null) - .map((view) => view.id), - ); - - const orphanViewNavigationMenuItemIds = Object.values( - flatNavigationMenuItemMaps.byUniversalIdentifier, - ) - .filter( - (item): item is NonNullable => - isDefined(item) && - item.type === NavigationMenuItemType.VIEW && - isDefined(item.viewId) && - !activeViewIds.has(item.viewId), - ) - .map((item) => item.id); - - if (orphanViewNavigationMenuItemIds.length === 0) { - return; - } - - if (options.dryRun) { - this.logger.log( - `[DRY RUN] Would delete ${orphanViewNavigationMenuItemIds.length} orphan navigation menu item(s) for workspace ${workspaceId}`, - ); - return; - } - - await this.navigationMenuItemRepository.delete({ - workspaceId, - id: In(orphanViewNavigationMenuItemIds), - }); - - this.logger.log( - `Deleted ${orphanViewNavigationMenuItemIds.length} orphan navigation menu item(s) for workspace ${workspaceId}`, - ); - - await this.workspaceCacheService.flush(workspaceId, [ - 'flatNavigationMenuItemMaps', - ]); - } -} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-identify-field-permission-metadata.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-identify-field-permission-metadata.command.ts deleted file mode 100644 index c4fdbc331d..0000000000 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-identify-field-permission-metadata.command.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { InjectDataSource } from '@nestjs/typeorm'; - -import { Command } from 'nest-commander'; -import { DataSource, IsNull } from 'typeorm'; -import { v4 } from 'uuid'; - -import { isDefined } from 'twenty-shared/utils'; - -import { ActiveOrSuspendedWorkspaceCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspace.command-runner'; -import { type RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspace.command-runner'; -import { WorkspaceIteratorService } from 'src/database/commands/command-runners/workspace-iterator.service'; -import { FieldPermissionEntity } from 'src/engine/metadata-modules/object-permission/field-permission/field-permission.entity'; - -@Command({ - name: 'upgrade:1-20:identify-field-permission-metadata', - description: - 'Identify field permission metadata (backfill universalIdentifier and applicationId)', -}) -export class IdentifyFieldPermissionMetadataCommand extends ActiveOrSuspendedWorkspaceCommandRunner { - private hasRunOnce = false; - - constructor( - protected readonly workspaceIteratorService: WorkspaceIteratorService, - @InjectDataSource() - private readonly coreDataSource: DataSource, - ) { - super(workspaceIteratorService); - } - - override async runOnWorkspace({ - options, - }: RunOnWorkspaceArgs): Promise { - if (this.hasRunOnce) { - this.logger.log( - 'Skipping has already been run once IdentifyFieldPermissionMetadataCommand', - ); - - return; - } - - if (options.dryRun) { - return; - } - - const queryRunner = this.coreDataSource.createQueryRunner(); - - await queryRunner.connect(); - await queryRunner.startTransaction(); - - try { - const repository = queryRunner.manager.getRepository( - FieldPermissionEntity, - ); - const withNullApplicationId = await repository.find({ - where: { applicationId: IsNull() }, - relations: ['role'], - }); - - const toUpdate = withNullApplicationId.filter((fieldPermission) => - isDefined(fieldPermission.role?.applicationId), - ); - const toRemove = withNullApplicationId.filter( - (fieldPermission) => !isDefined(fieldPermission.role?.applicationId), - ); - - for (const fieldPermission of toUpdate) { - fieldPermission.applicationId = fieldPermission.role!.applicationId; - fieldPermission.universalIdentifier = - fieldPermission.universalIdentifier ?? v4(); - } - - if (toUpdate.length > 0) { - await repository.save(toUpdate); - } - if (toRemove.length > 0) { - await repository.remove(toRemove); - } - - const withNullUniversalIdentifier = await repository.find({ - where: { universalIdentifier: IsNull() }, - }); - - for (const fieldPermission of withNullUniversalIdentifier) { - fieldPermission.universalIdentifier = v4(); - } - - if (withNullUniversalIdentifier.length > 0) { - await repository.save(withNullUniversalIdentifier); - } - - await queryRunner.commitTransaction(); - this.logger.log( - 'Successfully run IdentifyFieldPermissionMetadataCommand', - ); - this.hasRunOnce = true; - } catch (error) { - await queryRunner.rollbackTransaction(); - this.logger.error( - `Rolling back IdentifyFieldPermissionMetadataCommand: ${error.message}`, - ); - throw error; - } finally { - await queryRunner.release(); - } - } -} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-identify-object-permission-metadata.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-identify-object-permission-metadata.command.ts deleted file mode 100644 index 57b1885f0d..0000000000 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-identify-object-permission-metadata.command.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { InjectDataSource } from '@nestjs/typeorm'; - -import { Command } from 'nest-commander'; -import { DataSource, IsNull } from 'typeorm'; -import { v4 } from 'uuid'; - -import { isDefined } from 'twenty-shared/utils'; - -import { ActiveOrSuspendedWorkspaceCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspace.command-runner'; -import { type RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspace.command-runner'; -import { WorkspaceIteratorService } from 'src/database/commands/command-runners/workspace-iterator.service'; -import { ObjectPermissionEntity } from 'src/engine/metadata-modules/object-permission/object-permission.entity'; - -@Command({ - name: 'upgrade:1-20:identify-object-permission-metadata', - description: - 'Identify object permission metadata (backfill universalIdentifier and applicationId)', -}) -export class IdentifyObjectPermissionMetadataCommand extends ActiveOrSuspendedWorkspaceCommandRunner { - private hasRunOnce = false; - - constructor( - protected readonly workspaceIteratorService: WorkspaceIteratorService, - @InjectDataSource() - private readonly coreDataSource: DataSource, - ) { - super(workspaceIteratorService); - } - - override async runOnWorkspace({ - options, - }: RunOnWorkspaceArgs): Promise { - if (this.hasRunOnce) { - this.logger.log( - 'Skipping has already been run once IdentifyObjectPermissionMetadataCommand', - ); - - return; - } - - if (options.dryRun) { - return; - } - - const queryRunner = this.coreDataSource.createQueryRunner(); - - await queryRunner.connect(); - await queryRunner.startTransaction(); - - try { - const repository = queryRunner.manager.getRepository( - ObjectPermissionEntity, - ); - const withNullApplicationId = await repository.find({ - where: { applicationId: IsNull() }, - relations: ['role'], - }); - - const toUpdate = withNullApplicationId.filter((objectPermission) => - isDefined(objectPermission.role?.applicationId), - ); - const toRemove = withNullApplicationId.filter( - (objectPermission) => !isDefined(objectPermission.role?.applicationId), - ); - - for (const objectPermission of toUpdate) { - objectPermission.applicationId = objectPermission.role!.applicationId; - objectPermission.universalIdentifier = - objectPermission.universalIdentifier ?? v4(); - } - - if (toUpdate.length > 0) { - await repository.save(toUpdate); - } - if (toRemove.length > 0) { - await repository.remove(toRemove); - } - - const withNullUniversalIdentifier = await repository.find({ - where: { universalIdentifier: IsNull() }, - }); - - for (const objectPermission of withNullUniversalIdentifier) { - objectPermission.universalIdentifier = v4(); - } - - if (withNullUniversalIdentifier.length > 0) { - await repository.save(withNullUniversalIdentifier); - } - - await queryRunner.commitTransaction(); - this.logger.log( - 'Successfully run IdentifyObjectPermissionMetadataCommand', - ); - this.hasRunOnce = true; - } catch (error) { - await queryRunner.rollbackTransaction(); - this.logger.error( - `Rolling back IdentifyObjectPermissionMetadataCommand: ${error.message}`, - ); - throw error; - } finally { - await queryRunner.release(); - } - } -} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-identify-permission-flag-metadata.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-identify-permission-flag-metadata.command.ts deleted file mode 100644 index a65ba15fb3..0000000000 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-identify-permission-flag-metadata.command.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { InjectDataSource } from '@nestjs/typeorm'; - -import { Command } from 'nest-commander'; -import { DataSource, IsNull } from 'typeorm'; -import { v4 } from 'uuid'; - -import { isDefined } from 'twenty-shared/utils'; - -import { ActiveOrSuspendedWorkspaceCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspace.command-runner'; -import { type RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspace.command-runner'; -import { WorkspaceIteratorService } from 'src/database/commands/command-runners/workspace-iterator.service'; -import { PermissionFlagEntity } from 'src/engine/metadata-modules/permission-flag/permission-flag.entity'; - -@Command({ - name: 'upgrade:1-20:identify-permission-flag-metadata', - description: - 'Identify permission flag metadata (backfill universalIdentifier and applicationId)', -}) -export class IdentifyPermissionFlagMetadataCommand extends ActiveOrSuspendedWorkspaceCommandRunner { - private hasRunOnce = false; - - constructor( - protected readonly workspaceIteratorService: WorkspaceIteratorService, - @InjectDataSource() - private readonly coreDataSource: DataSource, - ) { - super(workspaceIteratorService); - } - - override async runOnWorkspace({ - options, - }: RunOnWorkspaceArgs): Promise { - if (this.hasRunOnce) { - this.logger.log( - 'Skipping has already been run once IdentifyPermissionFlagMetadataCommand', - ); - - return; - } - - if (options.dryRun) { - return; - } - - const queryRunner = this.coreDataSource.createQueryRunner(); - - await queryRunner.connect(); - await queryRunner.startTransaction(); - - try { - const repository = - queryRunner.manager.getRepository(PermissionFlagEntity); - const withNullApplicationId = await repository.find({ - where: { - applicationId: IsNull(), - }, - relations: ['role'], - }); - - const toUpdate = withNullApplicationId.filter((permissionFlag) => - isDefined(permissionFlag.role?.applicationId), - ); - const toRemove = withNullApplicationId.filter( - (permissionFlag) => !isDefined(permissionFlag.role?.applicationId), - ); - - for (const permissionFlag of toUpdate) { - const flag = permissionFlag; - flag.applicationId = permissionFlag.role.applicationId; - flag.universalIdentifier = flag.universalIdentifier ?? v4(); - } - - if (toUpdate.length > 0) { - await repository.save(toUpdate); - } - if (toRemove.length > 0) { - await repository.remove(toRemove); - } - - const withNullUniversalIdentifier = await repository.find({ - where: { - universalIdentifier: IsNull(), - }, - }); - - for (const permissionFlag of withNullUniversalIdentifier) { - permissionFlag.universalIdentifier = v4(); - } - - if (withNullUniversalIdentifier.length > 0) { - await repository.save(withNullUniversalIdentifier); - } - - await queryRunner.commitTransaction(); - this.logger.log('Successfully run IdentifyPermissionFlagMetadataCommand'); - this.hasRunOnce = true; - } catch (error) { - await queryRunner.rollbackTransaction(); - this.logger.error( - `Rolling back IdentifyPermissionFlagMetadataCommand: ${error.message}`, - ); - throw error; - } finally { - await queryRunner.release(); - } - } -} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-make-field-permission-universal-identifier-and-application-id-not-nullable-migration.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-make-field-permission-universal-identifier-and-application-id-not-nullable-migration.command.ts deleted file mode 100644 index acbbd7ad16..0000000000 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-make-field-permission-universal-identifier-and-application-id-not-nullable-migration.command.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { InjectDataSource } from '@nestjs/typeorm'; - -import { Command } from 'nest-commander'; -import { DataSource } from 'typeorm'; - -import { ActiveOrSuspendedWorkspaceCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspace.command-runner'; -import { type RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspace.command-runner'; -import { WorkspaceIteratorService } from 'src/database/commands/command-runners/workspace-iterator.service'; -import { makeFieldPermissionUniversalIdentifierAndApplicationIdNotNullQueries } from 'src/database/typeorm/core/migrations/utils/1773400000000-make-field-permission-universal-identifier-and-application-id-not-null.util'; - -@Command({ - name: 'upgrade:1-20:make-field-permission-universal-identifier-and-application-id-not-nullable-migration', - description: - 'Set NOT NULL on fieldPermission universalIdentifier and applicationId, add unique index and FK (run identify-field-permission-metadata first)', -}) -export class MakeFieldPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand extends ActiveOrSuspendedWorkspaceCommandRunner { - private hasRunOnce = false; - - constructor( - protected readonly workspaceIteratorService: WorkspaceIteratorService, - @InjectDataSource() - private readonly coreDataSource: DataSource, - ) { - super(workspaceIteratorService); - } - - override async runOnWorkspace({ - options, - }: RunOnWorkspaceArgs): Promise { - if (this.hasRunOnce) { - this.logger.log( - 'Skipping has already been run once MakeFieldPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand', - ); - - return; - } - - if (options.dryRun) { - return; - } - - const queryRunner = this.coreDataSource.createQueryRunner(); - - await queryRunner.connect(); - await queryRunner.startTransaction(); - - try { - await makeFieldPermissionUniversalIdentifierAndApplicationIdNotNullQueries( - queryRunner, - ); - - await queryRunner.commitTransaction(); - this.logger.log( - 'Successfully run MakeFieldPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand', - ); - this.hasRunOnce = true; - } catch (error) { - await queryRunner.rollbackTransaction(); - this.logger.error( - `Rolling back MakeFieldPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand: ${error.message}`, - ); - throw error; - } finally { - await queryRunner.release(); - } - } -} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-make-object-permission-universal-identifier-and-application-id-not-nullable-migration.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-make-object-permission-universal-identifier-and-application-id-not-nullable-migration.command.ts deleted file mode 100644 index b0e3f9fbb8..0000000000 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-make-object-permission-universal-identifier-and-application-id-not-nullable-migration.command.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { InjectDataSource } from '@nestjs/typeorm'; - -import { Command } from 'nest-commander'; -import { DataSource } from 'typeorm'; - -import { ActiveOrSuspendedWorkspaceCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspace.command-runner'; -import { type RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspace.command-runner'; -import { WorkspaceIteratorService } from 'src/database/commands/command-runners/workspace-iterator.service'; -import { makeObjectPermissionUniversalIdentifierAndApplicationIdNotNullQueries } from 'src/database/typeorm/core/migrations/utils/1773317160558-make-object-permission-universal-identifier-and-application-id-not-null.util'; - -@Command({ - name: 'upgrade:1-20:make-object-permission-universal-identifier-and-application-id-not-nullable-migration', - description: - 'Set NOT NULL on objectPermission universalIdentifier and applicationId, add unique index and FK (run identify-object-permission-metadata first)', -}) -export class MakeObjectPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand extends ActiveOrSuspendedWorkspaceCommandRunner { - private hasRunOnce = false; - - constructor( - protected readonly workspaceIteratorService: WorkspaceIteratorService, - @InjectDataSource() - private readonly coreDataSource: DataSource, - ) { - super(workspaceIteratorService); - } - - override async runOnWorkspace({ - options, - }: RunOnWorkspaceArgs): Promise { - if (this.hasRunOnce) { - this.logger.log( - 'Skipping has already been run once MakeObjectPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand', - ); - - return; - } - - if (options.dryRun) { - return; - } - - const queryRunner = this.coreDataSource.createQueryRunner(); - - await queryRunner.connect(); - await queryRunner.startTransaction(); - - try { - await makeObjectPermissionUniversalIdentifierAndApplicationIdNotNullQueries( - queryRunner, - ); - - await queryRunner.commitTransaction(); - this.logger.log( - 'Successfully run MakeObjectPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand', - ); - this.hasRunOnce = true; - } catch (error) { - await queryRunner.rollbackTransaction(); - this.logger.error( - `Rolling back MakeObjectPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand: ${error.message}`, - ); - throw error; - } finally { - await queryRunner.release(); - } - } -} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-make-permission-flag-universal-identifier-and-application-id-not-nullable-migration.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-make-permission-flag-universal-identifier-and-application-id-not-nullable-migration.command.ts deleted file mode 100644 index 18b8f0aa70..0000000000 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-make-permission-flag-universal-identifier-and-application-id-not-nullable-migration.command.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { InjectDataSource } from '@nestjs/typeorm'; - -import { Command } from 'nest-commander'; -import { DataSource } from 'typeorm'; - -import { ActiveOrSuspendedWorkspaceCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspace.command-runner'; -import { type RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspace.command-runner'; -import { WorkspaceIteratorService } from 'src/database/commands/command-runners/workspace-iterator.service'; -import { makePermissionFlagUniversalIdentifierAndApplicationIdNotNullQueries } from 'src/database/typeorm/core/migrations/utils/1773232418467-make-permission-flag-universal-identifier-and-application-id-not-null.util'; - -@Command({ - name: 'upgrade:1-20:make-permission-flag-universal-identifier-and-application-id-not-nullable-migration', - description: - 'Set NOT NULL on permissionFlag universalIdentifier and applicationId, add unique index and FK (run identify-permission-flag-metadata first)', -}) -export class MakePermissionFlagUniversalIdentifierAndApplicationIdNotNullableMigrationCommand extends ActiveOrSuspendedWorkspaceCommandRunner { - private hasRunOnce = false; - - constructor( - protected readonly workspaceIteratorService: WorkspaceIteratorService, - @InjectDataSource() - private readonly coreDataSource: DataSource, - ) { - super(workspaceIteratorService); - } - - override async runOnWorkspace({ - options, - }: RunOnWorkspaceArgs): Promise { - if (this.hasRunOnce) { - this.logger.log( - 'Skipping has already been run once MakePermissionFlagUniversalIdentifierAndApplicationIdNotNullableMigrationCommand', - ); - - return; - } - - if (options.dryRun) { - return; - } - - const queryRunner = this.coreDataSource.createQueryRunner(); - - await queryRunner.connect(); - await queryRunner.startTransaction(); - - try { - await makePermissionFlagUniversalIdentifierAndApplicationIdNotNullQueries( - queryRunner, - ); - - await queryRunner.commitTransaction(); - this.logger.log( - 'Successfully run MakePermissionFlagUniversalIdentifierAndApplicationIdNotNullableMigrationCommand', - ); - this.hasRunOnce = true; - } catch (error) { - await queryRunner.rollbackTransaction(); - this.logger.error( - `Rolling back MakePermissionFlagUniversalIdentifierAndApplicationIdNotNullableMigrationCommand: ${error.message}`, - ); - throw error; - } finally { - await queryRunner.release(); - } - } -} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-make-workflow-searchable.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-make-workflow-searchable.command.ts deleted file mode 100644 index 3a7450fc48..0000000000 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-make-workflow-searchable.command.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { InjectDataSource } from '@nestjs/typeorm'; - -import { Command } from 'nest-commander'; -import { DataSource } from 'typeorm'; - -import { ActiveOrSuspendedWorkspaceCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspace.command-runner'; -import { type RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspace.command-runner'; -import { WorkspaceIteratorService } from 'src/database/commands/command-runners/workspace-iterator.service'; -import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service'; -import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; -import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; - -@Command({ - name: 'upgrade:1-20:make-workflow-searchable', - description: 'Set isSearchable to true on the workflow object metadata', -}) -export class MakeWorkflowSearchableCommand extends ActiveOrSuspendedWorkspaceCommandRunner { - constructor( - protected readonly workspaceIteratorService: WorkspaceIteratorService, - @InjectDataSource() - private readonly coreDataSource: DataSource, - private readonly workspaceCacheService: WorkspaceCacheService, - private readonly workspaceCacheStorageService: WorkspaceCacheStorageService, - private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService, - ) { - super(workspaceIteratorService); - } - - override async runOnWorkspace({ - workspaceId, - options, - }: RunOnWorkspaceArgs): Promise { - const isDryRun = options.dryRun ?? false; - - if (isDryRun) { - this.logger.log( - `[DRY RUN] Would set isSearchable=true on workflow object for workspace ${workspaceId}. Skipping.`, - ); - - return; - } - - const queryRunner = this.coreDataSource.createQueryRunner(); - - await queryRunner.connect(); - - try { - const result = await queryRunner.query( - `UPDATE core."objectMetadata" - SET "isSearchable" = true - WHERE "workspaceId" = $1 - AND "nameSingular" = 'workflow' - AND "isSearchable" = false`, - [workspaceId], - ); - - const updatedCount = result?.[1] ?? 0; - - if (updatedCount > 0) { - this.logger.log( - `Set isSearchable=true on workflow object for workspace ${workspaceId}`, - ); - - await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [ - 'flatObjectMetadataMaps', - ]); - - await this.workspaceMetadataVersionService.incrementMetadataVersion( - workspaceId, - ); - - await this.workspaceCacheStorageService.flush(workspaceId); - } else { - this.logger.log( - `Workflow already searchable or not found for workspace ${workspaceId}, skipping`, - ); - } - } finally { - await queryRunner.release(); - } - } -} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-migrate-messaging-infrastructure-to-metadata.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-migrate-messaging-infrastructure-to-metadata.command.ts deleted file mode 100644 index 1591ddb4a8..0000000000 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-migrate-messaging-infrastructure-to-metadata.command.ts +++ /dev/null @@ -1,370 +0,0 @@ -import { InjectRepository } from '@nestjs/typeorm'; - -import { isNonEmptyString } from '@sniptt/guards'; -import { Command } from 'nest-commander'; -import { Repository } from 'typeorm'; - -import { ActiveOrSuspendedWorkspaceCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspace.command-runner'; -import { type RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspace.command-runner'; -import { WorkspaceIteratorService } from 'src/database/commands/command-runners/workspace-iterator.service'; -import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; -import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; -import { CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity'; -import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity'; -import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity'; -import { MessageFolderEntity } from 'src/engine/metadata-modules/message-folder/entities/message-folder.entity'; -import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; -import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; -import { type MessageFolderWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity'; -import { type WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; - -@Command({ - name: 'upgrade:1-20:migrate-messaging-infrastructure-to-metadata', - description: - 'Backfill connectedAccount, messageChannel, calendarChannel, and messageFolder to core metadata schema', -}) -export class MigrateMessagingInfrastructureToMetadataCommand extends ActiveOrSuspendedWorkspaceCommandRunner { - constructor( - private readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager, - @InjectRepository(ConnectedAccountEntity) - private readonly connectedAccountRepository: Repository, - @InjectRepository(MessageChannelEntity) - private readonly messageChannelRepository: Repository, - @InjectRepository(CalendarChannelEntity) - private readonly calendarChannelRepository: Repository, - @InjectRepository(MessageFolderEntity) - private readonly messageFolderRepository: Repository, - @InjectRepository(UserWorkspaceEntity) - private readonly userWorkspaceRepository: Repository, - private readonly featureFlagService: FeatureFlagService, - protected readonly workspaceIteratorService: WorkspaceIteratorService, - ) { - super(workspaceIteratorService); - } - - override async runOnWorkspace({ - workspaceId, - options, - }: RunOnWorkspaceArgs): Promise { - const isDryRun = options.dryRun ?? false; - - const connectedAccountWorkspaceRepository = - await this.twentyORMGlobalManager.getRepository( - workspaceId, - 'connectedAccount', - ); - - const messageChannelWorkspaceRepository = - await this.twentyORMGlobalManager.getRepository( - workspaceId, - 'messageChannel', - ); - - const calendarChannelWorkspaceRepository = - await this.twentyORMGlobalManager.getRepository( - workspaceId, - 'calendarChannel', - ); - - const messageFolderWorkspaceRepository = - await this.twentyORMGlobalManager.getRepository( - workspaceId, - 'messageFolder', - ); - - const connectedAccounts = await connectedAccountWorkspaceRepository.find(); - const messageChannels = await messageChannelWorkspaceRepository.find(); - const calendarChannels = await calendarChannelWorkspaceRepository.find(); - const messageFolders = await messageFolderWorkspaceRepository.find(); - - const workspaceMemberIdToUserWorkspaceIdMap = - await this.buildWorkspaceMemberIdToUserWorkspaceIdMap(workspaceId); - - const connectedAccountsWithMissingHandle = connectedAccounts.filter( - (account) => !account.handle, - ); - const connectedAccountsWithUnresolvedOwner = connectedAccounts.filter( - (account) => - !workspaceMemberIdToUserWorkspaceIdMap.has(account.accountOwnerId), - ); - const messageChannelsWithMissingHandle = messageChannels.filter( - (channel) => !channel.handle, - ); - const calendarChannelsWithMissingHandle = calendarChannels.filter( - (channel) => !channel.handle, - ); - - if (isDryRun) { - this.logger.log( - `[DRY RUN] Workspace ${workspaceId}: ` + - `${connectedAccounts.length} connected accounts, ` + - `${messageChannels.length} message channels, ` + - `${calendarChannels.length} calendar channels, ` + - `${messageFolders.length} message folders`, - ); - - if (connectedAccountsWithMissingHandle.length > 0) { - this.logger.warn( - `[DRY RUN] ${connectedAccountsWithMissingHandle.length} connected accounts have empty handle`, - ); - } - - if (connectedAccountsWithUnresolvedOwner.length > 0) { - this.logger.warn( - `[DRY RUN] ${connectedAccountsWithUnresolvedOwner.length} connected accounts have unresolvable accountOwnerId (no matching userWorkspace)`, - ); - } - - if (messageChannelsWithMissingHandle.length > 0) { - this.logger.warn( - `[DRY RUN] ${messageChannelsWithMissingHandle.length} message channels have empty handle`, - ); - } - - if (calendarChannelsWithMissingHandle.length > 0) { - this.logger.warn( - `[DRY RUN] ${calendarChannelsWithMissingHandle.length} calendar channels have empty handle`, - ); - } - - return; - } - - let migratedConnectedAccountIds = new Set( - connectedAccounts.map((account) => account.id), - ); - let migratedMessageChannelIds = new Set( - messageChannels.map((channel) => channel.id), - ); - - if (connectedAccounts.length > 0) { - const coreConnectedAccounts = connectedAccounts - .filter((workspaceEntity) => { - const userWorkspaceId = workspaceMemberIdToUserWorkspaceIdMap.get( - workspaceEntity.accountOwnerId, - ); - - if (!userWorkspaceId) { - this.logger.warn( - `Skipping connected account ${workspaceEntity.id}: no userWorkspace found for workspaceMember ${workspaceEntity.accountOwnerId}`, - ); - - return false; - } - - return true; - }) - .map((workspaceEntity) => { - const handleAliases = isNonEmptyString(workspaceEntity.handleAliases) - ? workspaceEntity.handleAliases - .split(',') - .map((alias) => alias.trim()) - : null; - - return { - id: workspaceEntity.id, - handle: workspaceEntity.handle ?? '', - provider: workspaceEntity.provider, - accessToken: workspaceEntity.accessToken, - refreshToken: workspaceEntity.refreshToken, - lastCredentialsRefreshedAt: - workspaceEntity.lastCredentialsRefreshedAt, - authFailedAt: workspaceEntity.authFailedAt, - handleAliases, - scopes: workspaceEntity.scopes, - connectionParameters: - workspaceEntity.connectionParameters as Record< - string, - unknown - > | null, - userWorkspaceId: workspaceMemberIdToUserWorkspaceIdMap.get( - workspaceEntity.accountOwnerId, - )!, - workspaceId, - createdAt: workspaceEntity.createdAt, - updatedAt: workspaceEntity.updatedAt, - }; - }); - - if (coreConnectedAccounts.length > 0) { - await this.connectedAccountRepository.save(coreConnectedAccounts); - this.logger.log( - `Migrated ${coreConnectedAccounts.length} connected accounts for workspace ${workspaceId}`, - ); - } - - migratedConnectedAccountIds = new Set( - coreConnectedAccounts.map((account) => account.id), - ); - } - - if (messageChannels.length > 0) { - const coreMessageChannels = messageChannels - .filter((workspaceEntity) => - migratedConnectedAccountIds.has(workspaceEntity.connectedAccountId), - ) - .map((workspaceEntity) => ({ - id: workspaceEntity.id, - visibility: workspaceEntity.visibility, - handle: workspaceEntity.handle ?? '', - type: workspaceEntity.type, - isContactAutoCreationEnabled: - workspaceEntity.isContactAutoCreationEnabled, - contactAutoCreationPolicy: workspaceEntity.contactAutoCreationPolicy, - messageFolderImportPolicy: workspaceEntity.messageFolderImportPolicy, - excludeNonProfessionalEmails: - workspaceEntity.excludeNonProfessionalEmails, - excludeGroupEmails: workspaceEntity.excludeGroupEmails, - pendingGroupEmailsAction: workspaceEntity.pendingGroupEmailsAction, - isSyncEnabled: workspaceEntity.isSyncEnabled, - syncCursor: workspaceEntity.syncCursor, - syncedAt: workspaceEntity.syncedAt - ? new Date(workspaceEntity.syncedAt) - : null, - syncStatus: workspaceEntity.syncStatus ?? 'NOT_SYNCED', - syncStage: workspaceEntity.syncStage, - syncStageStartedAt: workspaceEntity.syncStageStartedAt - ? new Date(workspaceEntity.syncStageStartedAt) - : null, - throttleFailureCount: workspaceEntity.throttleFailureCount, - throttleRetryAfter: workspaceEntity.throttleRetryAfter - ? new Date(workspaceEntity.throttleRetryAfter) - : null, - connectedAccountId: workspaceEntity.connectedAccountId, - workspaceId, - createdAt: workspaceEntity.createdAt, - updatedAt: workspaceEntity.updatedAt, - })); - - if (coreMessageChannels.length > 0) { - await this.messageChannelRepository.save( - coreMessageChannels as unknown as MessageChannelEntity[], - ); - this.logger.log( - `Migrated ${coreMessageChannels.length} message channels for workspace ${workspaceId}`, - ); - } - - migratedMessageChannelIds = new Set( - coreMessageChannels.map((channel) => channel.id), - ); - } - - if (calendarChannels.length > 0) { - const coreCalendarChannels = calendarChannels - .filter((workspaceEntity) => - migratedConnectedAccountIds.has(workspaceEntity.connectedAccountId), - ) - .map((workspaceEntity) => ({ - id: workspaceEntity.id, - handle: workspaceEntity.handle ?? '', - syncStatus: workspaceEntity.syncStatus ?? 'NOT_SYNCED', - syncStage: workspaceEntity.syncStage, - visibility: workspaceEntity.visibility, - isContactAutoCreationEnabled: - workspaceEntity.isContactAutoCreationEnabled, - contactAutoCreationPolicy: workspaceEntity.contactAutoCreationPolicy, - isSyncEnabled: workspaceEntity.isSyncEnabled, - syncCursor: workspaceEntity.syncCursor, - syncedAt: workspaceEntity.syncedAt - ? new Date(workspaceEntity.syncedAt) - : null, - syncStageStartedAt: workspaceEntity.syncStageStartedAt - ? new Date(workspaceEntity.syncStageStartedAt) - : null, - throttleFailureCount: workspaceEntity.throttleFailureCount, - connectedAccountId: workspaceEntity.connectedAccountId, - workspaceId, - createdAt: workspaceEntity.createdAt, - updatedAt: workspaceEntity.updatedAt, - })); - - await this.calendarChannelRepository.save( - coreCalendarChannels as unknown as CalendarChannelEntity[], - ); - this.logger.log( - `Migrated ${coreCalendarChannels.length} calendar channels for workspace ${workspaceId}`, - ); - } - - if (messageFolders.length > 0) { - const externalIdToFolderIdMap = new Map( - messageFolders.map((folder) => [folder.externalId, folder.id]), - ); - - const coreMessageFolders = messageFolders - .filter((workspaceEntity) => - migratedMessageChannelIds.has(workspaceEntity.messageChannelId), - ) - .map((workspaceEntity) => { - let resolvedParentFolderId: string | null = null; - - if (isNonEmptyString(workspaceEntity.parentFolderId)) { - resolvedParentFolderId = - externalIdToFolderIdMap.get(workspaceEntity.parentFolderId) ?? - null; - - if (!resolvedParentFolderId) { - this.logger.warn( - `Message folder ${workspaceEntity.id}: could not resolve parentFolderId externalId "${workspaceEntity.parentFolderId}" to a UUID`, - ); - } - } - - return { - id: workspaceEntity.id, - name: workspaceEntity.name, - syncCursor: workspaceEntity.syncCursor, - isSentFolder: workspaceEntity.isSentFolder, - isSynced: workspaceEntity.isSynced, - parentFolderId: resolvedParentFolderId, - externalId: workspaceEntity.externalId, - pendingSyncAction: workspaceEntity.pendingSyncAction, - messageChannelId: workspaceEntity.messageChannelId, - workspaceId, - createdAt: workspaceEntity.createdAt, - updatedAt: workspaceEntity.updatedAt, - }; - }); - - await this.messageFolderRepository.save( - coreMessageFolders as unknown as MessageFolderEntity[], - ); - this.logger.log( - `Migrated ${coreMessageFolders.length} message folders for workspace ${workspaceId}`, - ); - } - } - - private async buildWorkspaceMemberIdToUserWorkspaceIdMap( - workspaceId: string, - ): Promise> { - const workspaceMemberRepository = - await this.twentyORMGlobalManager.getRepository( - workspaceId, - 'workspaceMember', - ); - - const workspaceMembers = await workspaceMemberRepository.find(); - const userWorkspaces = await this.userWorkspaceRepository.find({ - where: { workspaceId }, - select: ['id', 'userId'], - }); - - const userWorkspaceIdByUserId = new Map( - userWorkspaces.map((userWorkspace) => [ - userWorkspace.userId, - userWorkspace.id, - ]), - ); - - return new Map( - workspaceMembers - .filter((member) => userWorkspaceIdByUserId.has(member.userId)) - .map((member) => [ - member.id, - userWorkspaceIdByUserId.get(member.userId)!, - ]), - ); - } -} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-migrate-rich-text-to-text.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-migrate-rich-text-to-text.command.ts deleted file mode 100644 index 5d9e0501f0..0000000000 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-migrate-rich-text-to-text.command.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { InjectDataSource } from '@nestjs/typeorm'; - -import { Command } from 'nest-commander'; -import { FeatureFlagKey } from 'twenty-shared/types'; -import { DataSource } from 'typeorm'; - -import { ActiveOrSuspendedWorkspaceCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspace.command-runner'; -import { type RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspace.command-runner'; -import { WorkspaceIteratorService } from 'src/database/commands/command-runners/workspace-iterator.service'; -import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; -import { getMetadataFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-flat-entity-maps-key.util'; -import { getMetadataRelatedMetadataNames } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-related-metadata-names.util'; -import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service'; -import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; -import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; -import { type WorkspaceCacheKeyName } from 'src/engine/workspace-cache/types/workspace-cache-key.type'; - -@Command({ - name: 'upgrade:1-20:migrate-rich-text-to-text', - description: - 'Migrate deprecated RICH_TEXT (V1) to TEXT and rename RICH_TEXT_V2 to RICH_TEXT. The underlying column type is already text, so only the metadata needs updating.', -}) -export class MigrateRichTextToTextCommand extends ActiveOrSuspendedWorkspaceCommandRunner { - constructor( - protected readonly workspaceIteratorService: WorkspaceIteratorService, - @InjectDataSource() - private readonly coreDataSource: DataSource, - private readonly featureFlagService: FeatureFlagService, - private readonly workspaceCacheService: WorkspaceCacheService, - private readonly workspaceCacheStorageService: WorkspaceCacheStorageService, - private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService, - ) { - super(workspaceIteratorService); - } - - override async runOnWorkspace({ - workspaceId, - options, - }: RunOnWorkspaceArgs): Promise { - const isMigrated = await this.featureFlagService.isFeatureEnabled( - FeatureFlagKey.IS_RICH_TEXT_V1_MIGRATED, - workspaceId, - ); - - if (isMigrated) { - this.logger.log( - `Rich text migration already completed for workspace ${workspaceId}. Skipping.`, - ); - - return; - } - - const dryRun = options?.dryRun ?? false; - - if (dryRun) { - this.logger.log( - `[DRY RUN] Would update RICH_TEXT -> TEXT and RICH_TEXT_V2 -> RICH_TEXT in core.fieldMetadata for workspace ${workspaceId}. Skipping.`, - ); - - return; - } - - this.logger.log(`Migrating RICH_TEXT fields in workspace ${workspaceId}`); - - const queryRunner = this.coreDataSource.createQueryRunner(); - - await queryRunner.connect(); - await queryRunner.startTransaction(); - - try { - const v1Result = await queryRunner.query( - `UPDATE core."fieldMetadata" - SET "type" = 'TEXT' - WHERE "workspaceId" = $1 - AND "type" = 'RICH_TEXT' - RETURNING "id"`, - [workspaceId], - ); - - const v1Count = v1Result.length; - - const renameResult = await queryRunner.query( - `UPDATE core."fieldMetadata" - SET "type" = 'RICH_TEXT' - WHERE "workspaceId" = $1 - AND "type" = 'RICH_TEXT_V2' - RETURNING "id"`, - [workspaceId], - ); - - const renameCount = renameResult.length; - - await queryRunner.commitTransaction(); - - if (v1Count > 0) { - this.logger.log( - `Migrated ${v1Count} RICH_TEXT (V1) field(s) to TEXT in workspace ${workspaceId}`, - ); - } - - if (renameCount > 0) { - this.logger.log( - `Renamed ${renameCount} RICH_TEXT_V2 field(s) to RICH_TEXT in workspace ${workspaceId}`, - ); - } - - await this.featureFlagService.enableFeatureFlags( - [FeatureFlagKey.IS_RICH_TEXT_V1_MIGRATED], - workspaceId, - ); - - if (v1Count > 0 || renameCount > 0) { - await this.invalidateCaches(workspaceId); - } else { - this.logger.log( - `No RICH_TEXT or RICH_TEXT_V2 fields found in workspace ${workspaceId}`, - ); - } - } catch (error) { - await queryRunner.rollbackTransaction(); - throw error; - } finally { - await queryRunner.release(); - } - } - - private async invalidateCaches(workspaceId: string): Promise { - const modifiedMetadataNames = ['fieldMetadata'] as const; - - const cacheKeysToInvalidate: WorkspaceCacheKeyName[] = [ - ...new Set( - modifiedMetadataNames - .flatMap((name) => [name, ...getMetadataRelatedMetadataNames(name)]) - .map(getMetadataFlatEntityMapsKey), - ), - 'ORMEntityMetadatas', - 'featureFlagsMap', - ]; - - await this.workspaceCacheService.invalidateAndRecompute( - workspaceId, - cacheKeysToInvalidate, - ); - - await this.workspaceMetadataVersionService.incrementMetadataVersion( - workspaceId, - ); - - await this.workspaceCacheStorageService.flush(workspaceId); - - this.logger.log( - `Cache invalidated and metadata version incremented for workspace ${workspaceId}`, - ); - } -} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-seed-cli-application-registration.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-seed-cli-application-registration.command.ts deleted file mode 100644 index 1271335115..0000000000 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-seed-cli-application-registration.command.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { Command } from 'nest-commander'; - -import { ActiveOrSuspendedWorkspaceCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspace.command-runner'; -import { type RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspace.command-runner'; -import { WorkspaceIteratorService } from 'src/database/commands/command-runners/workspace-iterator.service'; -import { ApplicationRegistrationService } from 'src/engine/core-modules/application/application-registration/application-registration.service'; - -@Command({ - name: 'upgrade:1-20:seed-cli-application-registration', - description: - 'Seed the Twenty CLI application registration for OAuth-based CLI login', -}) -export class SeedCliApplicationRegistrationCommand extends ActiveOrSuspendedWorkspaceCommandRunner { - private hasRun = false; - - constructor( - protected readonly workspaceIteratorService: WorkspaceIteratorService, - private readonly applicationRegistrationService: ApplicationRegistrationService, - ) { - super(workspaceIteratorService); - } - - override async runOnWorkspace({ - workspaceId: _, - options, - }: RunOnWorkspaceArgs): Promise { - const dryRun = options.dryRun ?? false; - - if (this.hasRun) { - return; - } - - if (dryRun) { - this.logger.log( - '[DRY RUN] Skipping CLI application registration seeding', - ); - return; - } - - const result = - await this.applicationRegistrationService.createCliRegistrationIfNotExists(); - - this.hasRun = true; - - if (result) { - this.logger.log( - `CLI application registration created (clientId: ${result.oAuthClientId})`, - ); - } else { - this.logger.log('CLI application registration already exists, skipping'); - } - } -} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-update-standard-index-view-names.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-update-standard-index-view-names.command.ts deleted file mode 100644 index c41ec889d3..0000000000 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-update-standard-index-view-names.command.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { InjectDataSource } from '@nestjs/typeorm'; - -import { Command } from 'nest-commander'; -import { DataSource } from 'typeorm'; - -import { ActiveOrSuspendedWorkspaceCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspace.command-runner'; -import { type RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspace.command-runner'; -import { WorkspaceIteratorService } from 'src/database/commands/command-runners/workspace-iterator.service'; - -@Command({ - name: 'upgrade:1-20:update-standard-index-view-names', - description: - 'Update standard index view names to use translatable template placeholders', -}) -export class UpdateStandardIndexViewNamesCommand extends ActiveOrSuspendedWorkspaceCommandRunner { - constructor( - protected readonly workspaceIteratorService: WorkspaceIteratorService, - @InjectDataSource() - private readonly coreDataSource: DataSource, - ) { - super(workspaceIteratorService); - } - - override async runOnWorkspace({ - workspaceId, - options, - }: RunOnWorkspaceArgs): Promise { - const dryRun = options?.dryRun ?? false; - - if (dryRun) { - this.logger.log( - `[DRY RUN] Would update standard index view names for workspace ${workspaceId}. Skipping.`, - ); - - return; - } - - const queryRunner = this.coreDataSource.createQueryRunner(); - - await queryRunner.connect(); - await queryRunner.startTransaction(); - - try { - const result = await queryRunner.query( - ` - UPDATE core."view" - SET name = 'All {objectLabelPlural}' - WHERE "workspaceId" = $1 - AND "isCustom" = false - AND key = 'INDEX' - AND name LIKE 'All %' - AND name != 'All {objectLabelPlural}' - `, - [workspaceId], - ); - - const updateCount = result?.[1] ?? 0; - - if (updateCount === 0) { - this.logger.log( - `No standard index views needed updating for workspace ${workspaceId}`, - ); - } else { - this.logger.log( - `Updated ${updateCount} standard index view(s) for workspace ${workspaceId}`, - ); - } - - await queryRunner.commitTransaction(); - } catch (error) { - if (queryRunner.isTransactionActive) { - await queryRunner.rollbackTransaction(); - } - this.logger.error( - `Error updating standard index view names for workspace ${workspaceId}`, - error, - ); - throw error; - } finally { - await queryRunner.release(); - } - } -} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-upgrade-version-command.module.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-upgrade-version-command.module.ts deleted file mode 100644 index 898d779f1f..0000000000 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-upgrade-version-command.module.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { Module } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; - -import { WorkspaceIteratorModule } from 'src/database/commands/command-runners/workspace-iterator.module'; -import { BackfillCommandMenuItemsCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-backfill-command-menu-items.command'; -import { BackfillNavigationMenuItemTypeCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-backfill-navigation-menu-item-type.command'; -import { BackfillSelectFieldOptionIdsCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-backfill-select-field-option-ids.command'; -import { DeleteOrphanNavigationMenuItemsCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-delete-orphan-navigation-menu-items.command'; -import { IdentifyFieldPermissionMetadataCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-identify-field-permission-metadata.command'; -import { IdentifyObjectPermissionMetadataCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-identify-object-permission-metadata.command'; -import { IdentifyPermissionFlagMetadataCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-identify-permission-flag-metadata.command'; -import { MakeFieldPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-make-field-permission-universal-identifier-and-application-id-not-nullable-migration.command'; -import { MakeObjectPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-make-object-permission-universal-identifier-and-application-id-not-nullable-migration.command'; -import { MakePermissionFlagUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-make-permission-flag-universal-identifier-and-application-id-not-nullable-migration.command'; -import { MakeWorkflowSearchableCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-make-workflow-searchable.command'; -import { MigrateMessagingInfrastructureToMetadataCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-migrate-messaging-infrastructure-to-metadata.command'; -import { MigrateRichTextToTextCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-migrate-rich-text-to-text.command'; -import { SeedCliApplicationRegistrationCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-seed-cli-application-registration.command'; -import { UpdateStandardIndexViewNamesCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-update-standard-index-view-names.command'; -import { ApplicationRegistrationModule } from 'src/engine/core-modules/application/application-registration/application-registration.module'; -import { ApplicationModule } from 'src/engine/core-modules/application/application.module'; -import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; -import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; -import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; -import { CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity'; -import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity'; -import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; -import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity'; -import { MessageFolderEntity } from 'src/engine/metadata-modules/message-folder/entities/message-folder.entity'; -import { NavigationMenuItemEntity } from 'src/engine/metadata-modules/navigation-menu-item/entities/navigation-menu-item.entity'; -import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module'; -import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module'; -import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module'; -import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/workspace-migration-runner.module'; -import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module'; -import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-common.module'; - -@Module({ - imports: [ - TypeOrmModule.forFeature([ - WorkspaceEntity, - ConnectedAccountEntity, - MessageChannelEntity, - CalendarChannelEntity, - MessageFolderEntity, - UserWorkspaceEntity, - NavigationMenuItemEntity, - ]), - DataSourceModule, - WorkspaceCacheModule, - WorkspaceCacheStorageModule, - WorkspaceMetadataVersionModule, - WorkspaceMigrationRunnerModule, - ApplicationModule, - ApplicationRegistrationModule, - WorkspaceMigrationModule, - FeatureFlagModule, - WorkflowCommonModule, - WorkspaceIteratorModule, - ], - providers: [ - IdentifyPermissionFlagMetadataCommand, - MakePermissionFlagUniversalIdentifierAndApplicationIdNotNullableMigrationCommand, - IdentifyObjectPermissionMetadataCommand, - MakeObjectPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand, - IdentifyFieldPermissionMetadataCommand, - MakeFieldPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand, - BackfillCommandMenuItemsCommand, - BackfillNavigationMenuItemTypeCommand, - BackfillSelectFieldOptionIdsCommand, - DeleteOrphanNavigationMenuItemsCommand, - SeedCliApplicationRegistrationCommand, - MigrateRichTextToTextCommand, - MigrateMessagingInfrastructureToMetadataCommand, - UpdateStandardIndexViewNamesCommand, - MakeWorkflowSearchableCommand, - ], - exports: [ - IdentifyPermissionFlagMetadataCommand, - MakePermissionFlagUniversalIdentifierAndApplicationIdNotNullableMigrationCommand, - IdentifyObjectPermissionMetadataCommand, - MakeObjectPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand, - IdentifyFieldPermissionMetadataCommand, - MakeFieldPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand, - BackfillCommandMenuItemsCommand, - BackfillNavigationMenuItemTypeCommand, - BackfillSelectFieldOptionIdsCommand, - DeleteOrphanNavigationMenuItemsCommand, - SeedCliApplicationRegistrationCommand, - MigrateRichTextToTextCommand, - MigrateMessagingInfrastructureToMetadataCommand, - UpdateStandardIndexViewNamesCommand, - MakeWorkflowSearchableCommand, - ], -}) -export class V1_20_UpgradeVersionCommandModule {} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-upgrade-version-command.module.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-upgrade-version-command.module.ts index 9fc4641c32..ede6477098 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-upgrade-version-command.module.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-upgrade-version-command.module.ts @@ -2,19 +2,19 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { WorkspaceIteratorModule } from 'src/database/commands/command-runners/workspace-iterator.module'; -import { AddComposeEmailCommandMenuItemCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-add-compose-email-command-menu-item.command'; -import { MigrateMessagingInfrastructureToMetadataCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-migrate-messaging-infrastructure-to-metadata.command'; -import { RefactorNavigationCommandsCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-refactor-navigation-commands.command'; -import { AddGlobalKeyValuePairUniqueIndexCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-add-global-key-value-pair-unique-index.command'; -import { BackfillDatasourceToWorkspaceCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-backfill-datasource-to-workspace.command'; -import { BackfillMessageThreadSubjectCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-backfill-message-thread-subject.command'; -import { BackfillPageLayoutsAndFieldsWidgetViewFieldsCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-backfill-page-layouts-and-fields-widget-view-fields.command'; -import { DeduplicateEngineCommandsCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-deduplicate-engine-commands.command'; -import { DropWorkspaceMessagingFksCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-drop-workspace-messaging-fks.command'; -import { FixSelectAllCommandMenuItemsCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-fix-select-all-command-menu-items.command'; -import { MigrateAiAgentTextToJsonResponseFormatCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-migrate-ai-agent-text-to-json-response-format.command'; -import { MigrateMessageFolderParentIdToExternalIdCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-migrate-message-folder-parent-id-to-external-id.command'; -import { UpdateEditLayoutCommandMenuItemLabelCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-update-edit-layout-command-menu-item-label.command'; +import { RefactorNavigationCommandsCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-1775500013000-refactor-navigation-commands.command'; +import { AddComposeEmailCommandMenuItemCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-1775500001000-add-compose-email-command-menu-item.command'; +import { AddGlobalKeyValuePairUniqueIndexCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-1775500002000-add-global-key-value-pair-unique-index.command'; +import { BackfillDatasourceToWorkspaceCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-1775500003000-backfill-datasource-to-workspace.command'; +import { BackfillMessageThreadSubjectCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-1775500004000-backfill-message-thread-subject.command'; +import { BackfillPageLayoutsAndFieldsWidgetViewFieldsCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-1775500005000-backfill-page-layouts-and-fields-widget-view-fields.command'; +import { DeduplicateEngineCommandsCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-1775500006000-deduplicate-engine-commands.command'; +import { FixSelectAllCommandMenuItemsCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-1775500007000-fix-select-all-command-menu-items.command'; +import { MigrateAiAgentTextToJsonResponseFormatCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-1775500008000-migrate-ai-agent-text-to-json-response-format.command'; +import { UpdateEditLayoutCommandMenuItemLabelCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-1775500009000-update-edit-layout-command-menu-item-label.command'; +import { DropWorkspaceMessagingFksCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-1775500010000-drop-workspace-messaging-fks.command'; +import { MigrateMessageFolderParentIdToExternalIdCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-1775500011000-migrate-message-folder-parent-id-to-external-id.command'; +import { MigrateMessagingInfrastructureToMetadataCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-1775500012000-migrate-messaging-infrastructure-to-metadata.command'; import { ApplicationModule } from 'src/engine/core-modules/application/application.module'; import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; @@ -65,20 +65,5 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace MigrateMessageFolderParentIdToExternalIdCommand, MigrateMessagingInfrastructureToMetadataCommand, ], - exports: [ - AddComposeEmailCommandMenuItemCommand, - AddGlobalKeyValuePairUniqueIndexCommand, - BackfillDatasourceToWorkspaceCommand, - BackfillMessageThreadSubjectCommand, - BackfillPageLayoutsAndFieldsWidgetViewFieldsCommand, - DeduplicateEngineCommandsCommand, - FixSelectAllCommandMenuItemsCommand, - MigrateAiAgentTextToJsonResponseFormatCommand, - UpdateEditLayoutCommandMenuItemLabelCommand, - RefactorNavigationCommandsCommand, - DropWorkspaceMessagingFksCommand, - MigrateMessageFolderParentIdToExternalIdCommand, - MigrateMessagingInfrastructureToMetadataCommand, - ], }) export class V1_21_UpgradeVersionCommandModule {} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-add-compose-email-command-menu-item.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-1775500001000-add-compose-email-command-menu-item.command.ts similarity index 96% rename from packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-add-compose-email-command-menu-item.command.ts rename to packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-1775500001000-add-compose-email-command-menu-item.command.ts index 92b433abab..9ffabf41d8 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-add-compose-email-command-menu-item.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-1775500001000-add-compose-email-command-menu-item.command.ts @@ -4,6 +4,7 @@ import { isDefined } from 'twenty-shared/utils'; import { ActiveOrSuspendedWorkspaceCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspace.command-runner'; import { WorkspaceIteratorService } from 'src/database/commands/command-runners/workspace-iterator.service'; import { type RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspace.command-runner'; +import { RegisteredWorkspaceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-workspace-command.decorator'; import { ApplicationService } from 'src/engine/core-modules/application/application.service'; import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; import { STANDARD_COMMAND_MENU_ITEMS } from 'src/engine/workspace-manager/twenty-standard-application/constants/standard-command-menu-item.constant'; @@ -13,6 +14,7 @@ import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspa const COMPOSE_EMAIL_UNIVERSAL_IDENTIFIER = STANDARD_COMMAND_MENU_ITEMS.composeEmail.universalIdentifier; +@RegisteredWorkspaceCommand('1.21.0', 1775500001000) @Command({ name: 'upgrade:1-21:add-compose-email-command-menu-item', description: 'Add the Compose Email command menu item to existing workspaces', diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-add-global-key-value-pair-unique-index.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-1775500002000-add-global-key-value-pair-unique-index.command.ts similarity index 94% rename from packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-add-global-key-value-pair-unique-index.command.ts rename to packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-1775500002000-add-global-key-value-pair-unique-index.command.ts index 35680e9ee9..dd920188b0 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-add-global-key-value-pair-unique-index.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-1775500002000-add-global-key-value-pair-unique-index.command.ts @@ -6,8 +6,10 @@ import { DataSource } from 'typeorm'; import { ActiveOrSuspendedWorkspaceCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspace.command-runner'; import { WorkspaceIteratorService } from 'src/database/commands/command-runners/workspace-iterator.service'; import { type RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspace.command-runner'; +import { RegisteredWorkspaceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-workspace-command.decorator'; import { addGlobalKeyValuePairUniqueIndexQueries } from 'src/database/typeorm/core/migrations/utils/1774700000000-add-global-key-value-pair-unique-index.util'; +@RegisteredWorkspaceCommand('1.21.0', 1775500002000) @Command({ name: 'upgrade:1-21:add-global-key-value-pair-unique-index', description: diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-backfill-datasource-to-workspace.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-1775500003000-backfill-datasource-to-workspace.command.ts similarity index 94% rename from packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-backfill-datasource-to-workspace.command.ts rename to packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-1775500003000-backfill-datasource-to-workspace.command.ts index 242dbb9b3e..66410c9567 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-backfill-datasource-to-workspace.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-1775500003000-backfill-datasource-to-workspace.command.ts @@ -8,9 +8,11 @@ import { Repository } from 'typeorm'; import { ActiveOrSuspendedWorkspaceCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspace.command-runner'; import { WorkspaceIteratorService } from 'src/database/commands/command-runners/workspace-iterator.service'; import { type RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspace.command-runner'; +import { RegisteredWorkspaceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-workspace-command.decorator'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity'; +@RegisteredWorkspaceCommand('1.21.0', 1775500003000) @Command({ name: 'upgrade:1-21:backfill-datasource-to-workspace', description: diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-backfill-message-thread-subject.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-1775500004000-backfill-message-thread-subject.command.ts similarity index 97% rename from packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-backfill-message-thread-subject.command.ts rename to packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-1775500004000-backfill-message-thread-subject.command.ts index e0a0830100..96c881cdd4 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-backfill-message-thread-subject.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-1775500004000-backfill-message-thread-subject.command.ts @@ -5,6 +5,7 @@ import { FieldMetadataType } from 'twenty-shared/types'; import { ActiveOrSuspendedWorkspaceCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspace.command-runner'; import { WorkspaceIteratorService } from 'src/database/commands/command-runners/workspace-iterator.service'; import { type RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspace.command-runner'; +import { RegisteredWorkspaceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-workspace-command.decorator'; import { ApplicationService } from 'src/engine/core-modules/application/application.service'; import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.util'; import { getDefaultFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/get-default-flat-field-metadata-from-create-field-input.util'; @@ -13,6 +14,7 @@ import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/works import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util'; import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service'; +@RegisteredWorkspaceCommand('1.21.0', 1775500004000) @Command({ name: 'upgrade:1-21:backfill-message-thread-subject', description: diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-backfill-page-layouts-and-fields-widget-view-fields.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-1775500005000-backfill-page-layouts-and-fields-widget-view-fields.command.ts similarity index 99% rename from packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-backfill-page-layouts-and-fields-widget-view-fields.command.ts rename to packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-1775500005000-backfill-page-layouts-and-fields-widget-view-fields.command.ts index 82ab2ca680..96301acba2 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-backfill-page-layouts-and-fields-widget-view-fields.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-1775500005000-backfill-page-layouts-and-fields-widget-view-fields.command.ts @@ -12,6 +12,7 @@ import { v4 } from 'uuid'; import { ActiveOrSuspendedWorkspaceCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspace.command-runner'; import { WorkspaceIteratorService } from 'src/database/commands/command-runners/workspace-iterator.service'; import { type RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspace.command-runner'; +import { RegisteredWorkspaceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-workspace-command.decorator'; import { ApplicationService } from 'src/engine/core-modules/application/application.service'; import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type'; import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; @@ -81,6 +82,7 @@ const isRelationTargetAvailable = ( return true; }; +@RegisteredWorkspaceCommand('1.21.0', 1775500005000) @Command({ name: 'upgrade:1-21:backfill-page-layouts-and-fields-widget-view-fields', description: diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-deduplicate-engine-commands.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-1775500006000-deduplicate-engine-commands.command.ts similarity index 97% rename from packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-deduplicate-engine-commands.command.ts rename to packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-1775500006000-deduplicate-engine-commands.command.ts index ff996cffb3..59bdedb3ed 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-deduplicate-engine-commands.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-1775500006000-deduplicate-engine-commands.command.ts @@ -4,6 +4,7 @@ import { isDefined } from 'twenty-shared/utils'; import { ActiveOrSuspendedWorkspaceCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspace.command-runner'; import { WorkspaceIteratorService } from 'src/database/commands/command-runners/workspace-iterator.service'; import { type RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspace.command-runner'; +import { RegisteredWorkspaceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-workspace-command.decorator'; import { ApplicationService } from 'src/engine/core-modules/application/application.service'; import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; import { STANDARD_COMMAND_MENU_ITEMS } from 'src/engine/workspace-manager/twenty-standard-application/constants/standard-command-menu-item.constant'; @@ -29,6 +30,7 @@ const NEW_UNIVERSAL_IDENTIFIERS = new Set([ STANDARD_COMMAND_MENU_ITEMS.exportRecords.universalIdentifier, ]); +@RegisteredWorkspaceCommand('1.21.0', 1775500006000) @Command({ name: 'upgrade:1-21:deduplicate-engine-commands', description: diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-fix-select-all-command-menu-items.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-1775500007000-fix-select-all-command-menu-items.command.ts similarity index 96% rename from packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-fix-select-all-command-menu-items.command.ts rename to packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-1775500007000-fix-select-all-command-menu-items.command.ts index a307afa1f1..e55503c71c 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-fix-select-all-command-menu-items.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-1775500007000-fix-select-all-command-menu-items.command.ts @@ -4,6 +4,7 @@ import { isDefined } from 'twenty-shared/utils'; import { ActiveOrSuspendedWorkspaceCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspace.command-runner'; import { WorkspaceIteratorService } from 'src/database/commands/command-runners/workspace-iterator.service'; import { type RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspace.command-runner'; +import { RegisteredWorkspaceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-workspace-command.decorator'; import { ApplicationService } from 'src/engine/core-modules/application/application.service'; import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; import { STANDARD_COMMAND_MENU_ITEMS } from 'src/engine/workspace-manager/twenty-standard-application/constants/standard-command-menu-item.constant'; @@ -16,6 +17,7 @@ const UNIVERSAL_IDENTIFIERS_TO_FIX = new Set([ STANDARD_COMMAND_MENU_ITEMS.destroyRecords.universalIdentifier, ]); +@RegisteredWorkspaceCommand('1.21.0', 1775500007000) @Command({ name: 'upgrade:1-21:fix-select-all-command-menu-items', description: diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-migrate-ai-agent-text-to-json-response-format.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-1775500008000-migrate-ai-agent-text-to-json-response-format.command.ts similarity index 97% rename from packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-migrate-ai-agent-text-to-json-response-format.command.ts rename to packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-1775500008000-migrate-ai-agent-text-to-json-response-format.command.ts index 323b6f1f75..2765b938fc 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-migrate-ai-agent-text-to-json-response-format.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-1775500008000-migrate-ai-agent-text-to-json-response-format.command.ts @@ -4,6 +4,7 @@ import { isDefined } from 'twenty-shared/utils'; import { ActiveOrSuspendedWorkspaceCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspace.command-runner'; import { WorkspaceIteratorService } from 'src/database/commands/command-runners/workspace-iterator.service'; import { type RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspace.command-runner'; +import { RegisteredWorkspaceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-workspace-command.decorator'; import { ApplicationService } from 'src/engine/core-modules/application/application.service'; import { type FlatAgent } from 'src/engine/metadata-modules/flat-agent/types/flat-agent.type'; import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; @@ -36,6 +37,7 @@ const TEXT_AGENT_DEFAULT_OUTPUT_SCHEMA = { }, }; +@RegisteredWorkspaceCommand('1.21.0', 1775500008000) @Command({ name: 'upgrade:1-21:migrate-ai-agent-text-to-json-response-format', description: diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-update-edit-layout-command-menu-item-label.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-1775500009000-update-edit-layout-command-menu-item-label.command.ts similarity index 93% rename from packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-update-edit-layout-command-menu-item-label.command.ts rename to packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-1775500009000-update-edit-layout-command-menu-item-label.command.ts index 1c06e7decb..7d2d48866b 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-update-edit-layout-command-menu-item-label.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-1775500009000-update-edit-layout-command-menu-item-label.command.ts @@ -6,10 +6,12 @@ import { DataSource } from 'typeorm'; import { ActiveOrSuspendedWorkspaceCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspace.command-runner'; import { WorkspaceIteratorService } from 'src/database/commands/command-runners/workspace-iterator.service'; import { type RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspace.command-runner'; +import { RegisteredWorkspaceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-workspace-command.decorator'; const EDIT_RECORD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER = 'd9794c67-1799-424f-8871-5ea771dd4a6d'; +@RegisteredWorkspaceCommand('1.21.0', 1775500009000) @Command({ name: 'upgrade:1-21:update-edit-layout-command-menu-item-label', description: 'Update Edit Page Layout command menu item label to Edit Layout', diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-drop-workspace-messaging-fks.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-1775500010000-drop-workspace-messaging-fks.command.ts similarity index 94% rename from packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-drop-workspace-messaging-fks.command.ts rename to packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-1775500010000-drop-workspace-messaging-fks.command.ts index a63c57a04f..7cb588d550 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-drop-workspace-messaging-fks.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-1775500010000-drop-workspace-messaging-fks.command.ts @@ -3,6 +3,7 @@ import { Command } from 'nest-commander'; import { ActiveOrSuspendedWorkspaceCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspace.command-runner'; import { WorkspaceIteratorService } from 'src/database/commands/command-runners/workspace-iterator.service'; import { type RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspace.command-runner'; +import { RegisteredWorkspaceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-workspace-command.decorator'; import { WorkspaceSchemaManagerService } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.service'; import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util'; @@ -22,6 +23,7 @@ const FK_COLUMNS_TO_DROP = [ }, ]; +@RegisteredWorkspaceCommand('1.21.0', 1775500010000) @Command({ name: 'upgrade:1-21:drop-workspace-messaging-fks', description: diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-migrate-message-folder-parent-id-to-external-id.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-1775500011000-migrate-message-folder-parent-id-to-external-id.command.ts similarity index 94% rename from packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-migrate-message-folder-parent-id-to-external-id.command.ts rename to packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-1775500011000-migrate-message-folder-parent-id-to-external-id.command.ts index 4231ba386a..f32a6c1904 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-migrate-message-folder-parent-id-to-external-id.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-1775500011000-migrate-message-folder-parent-id-to-external-id.command.ts @@ -6,11 +6,13 @@ import { Repository } from 'typeorm'; import { ActiveOrSuspendedWorkspaceCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspace.command-runner'; import { WorkspaceIteratorService } from 'src/database/commands/command-runners/workspace-iterator.service'; import { type RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspace.command-runner'; +import { RegisteredWorkspaceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-workspace-command.decorator'; import { MessageFolderEntity } from 'src/engine/metadata-modules/message-folder/entities/message-folder.entity'; const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; +@RegisteredWorkspaceCommand('1.21.0', 1775500011000) @Command({ name: 'upgrade:1-21:migrate-message-folder-parent-id-to-external-id', description: diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-migrate-messaging-infrastructure-to-metadata.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-1775500012000-migrate-messaging-infrastructure-to-metadata.command.ts similarity index 98% rename from packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-migrate-messaging-infrastructure-to-metadata.command.ts rename to packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-1775500012000-migrate-messaging-infrastructure-to-metadata.command.ts index e5e560f8ff..4a8ec2a326 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-migrate-messaging-infrastructure-to-metadata.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-1775500012000-migrate-messaging-infrastructure-to-metadata.command.ts @@ -8,6 +8,7 @@ import { Repository } from 'typeorm'; import { ActiveOrSuspendedWorkspaceCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspace.command-runner'; import { type RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspace.command-runner'; import { WorkspaceIteratorService } from 'src/database/commands/command-runners/workspace-iterator.service'; +import { RegisteredWorkspaceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-workspace-command.decorator'; import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; import { CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity'; @@ -19,6 +20,7 @@ import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-acco import { type MessageFolderWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity'; import { type WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; +@RegisteredWorkspaceCommand('1.21.0', 1775500012000) @Command({ name: 'upgrade:1-21:migrate-messaging-infrastructure-to-metadata', description: diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-refactor-navigation-commands.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-1775500013000-refactor-navigation-commands.command.ts similarity index 98% rename from packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-refactor-navigation-commands.command.ts rename to packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-1775500013000-refactor-navigation-commands.command.ts index 465bf9e8d3..819c99dc71 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-refactor-navigation-commands.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-1775500013000-refactor-navigation-commands.command.ts @@ -13,6 +13,7 @@ import { } from 'src/database/commands/command-runners/workspace.command-runner'; import { addPayloadCheckConstraintToCommandMenuItem } from 'src/database/typeorm/core/migrations/utils/1775129635528-add-payload-to-command-menu-item.util'; import { ApplicationService } from 'src/engine/core-modules/application/application.service'; +import { RegisteredWorkspaceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-workspace-command.decorator'; import { EngineComponentKey } from 'src/engine/metadata-modules/command-menu-item/enums/engine-component-key.enum'; import { type FlatCommandMenuItem } from 'src/engine/metadata-modules/flat-command-menu-item/types/flat-command-menu-item.type'; import { @@ -57,6 +58,7 @@ const SETTINGS_NAVIGATION_ITEM_KEYS = [ 'goToSettingsUpdates', ] as const satisfies ReadonlyArray; +@RegisteredWorkspaceCommand('1.21.0', 1775500013000) @Command({ name: 'upgrade:1-21:refactor-navigation-commands', description: diff --git a/packages/twenty-server/src/database/commands/command-runners/__tests__/upgrade.command-runner.spec.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/__tests__/upgrade.command.spec.ts similarity index 90% rename from packages/twenty-server/src/database/commands/command-runners/__tests__/upgrade.command-runner.spec.ts rename to packages/twenty-server/src/database/commands/upgrade-version-command/__tests__/upgrade.command.spec.ts index 4423b0cda9..600a7eabf9 100644 --- a/packages/twenty-server/src/database/commands/command-runners/__tests__/upgrade.command-runner.spec.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/__tests__/upgrade.command.spec.ts @@ -11,20 +11,19 @@ import { import { getDataSourceToken } from '@nestjs/typeorm'; -import { - UpgradeCommandOptions, - UpgradeCommandRunner, - type AllCommands, -} from 'src/database/commands/command-runners/upgrade.command-runner'; import { WorkspaceIteratorService } from 'src/database/commands/command-runners/workspace-iterator.service'; -import { RegisteredInstanceMigrationService } from 'src/engine/core-modules/upgrade/services/registered-instance-migration-registry.service'; -import { WorkspaceUpgradeService } from 'src/engine/core-modules/upgrade/services/workspace-upgrade.service'; -import { RegisteredInstanceMigration } from 'src/database/typeorm/core/decorators/registered-instance-migration.decorator'; +import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator'; +import { + UpgradeCommand, + UpgradeCommandOptions, +} from 'src/database/commands/upgrade-version-command/upgrade.command'; import { UPGRADE_COMMAND_SUPPORTED_VERSIONS } from 'src/engine/constants/upgrade-command-supported-versions.constant'; import { CoreEngineVersionService } from 'src/engine/core-engine-version/services/core-engine-version.service'; import { type ConfigVariables } from 'src/engine/core-modules/twenty-config/config-variables'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; import { InstanceUpgradeService } from 'src/engine/core-modules/upgrade/services/instance-upgrade.service'; +import { UpgradeCommandRegistryService } from 'src/engine/core-modules/upgrade/services/upgrade-command-registry.service'; +import { WorkspaceUpgradeService } from 'src/engine/core-modules/upgrade/services/workspace-upgrade.service'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { WorkspaceVersionService } from 'src/engine/workspace-manager/workspace-version/services/workspace-version.service'; import { compareVersionMajorAndMinor } from 'src/utils/version/compare-version-minor-and-major'; @@ -38,13 +37,7 @@ const PREVIOUS_VERSION = UPGRADE_COMMAND_SUPPORTED_VERSIONS.length - 2 ]; -class BasicUpgradeCommandRunner extends UpgradeCommandRunner { - allCommands = Object.fromEntries( - UPGRADE_COMMAND_SUPPORTED_VERSIONS.map((version) => [version, []]), - ) as unknown as AllCommands; -} - -type CommandRunnerValues = typeof BasicUpgradeCommandRunner; +type CommandRunnerValues = typeof UpgradeCommand; const generateMockWorkspace = (overrides?: Partial) => ({ @@ -78,7 +71,7 @@ const buildUpgradeCommandModule = async ({ }: BuildUpgradeCommandModuleArgs) => { const registryProvider = migrations ? { - provide: RegisteredInstanceMigrationService, + provide: UpgradeCommandRegistryService, useFactory: () => { const fakeDiscoveryService = { getProviders: () => @@ -87,7 +80,7 @@ const buildUpgradeCommandModule = async ({ metatype: migration.constructor, })), } as unknown as import('@nestjs/core').DiscoveryService; - const registry = new RegisteredInstanceMigrationService( + const registry = new UpgradeCommandRegistryService( fakeDiscoveryService, ); @@ -97,9 +90,10 @@ const buildUpgradeCommandModule = async ({ }, } : { - provide: RegisteredInstanceMigrationService, + provide: UpgradeCommandRegistryService, useValue: { getInstanceCommandsForVersion: jest.fn().mockReturnValue([]), + getWorkspaceCommandsForVersion: jest.fn().mockReturnValue([]), }, }; @@ -116,7 +110,7 @@ const buildUpgradeCommandModule = async ({ useFactory: ( coreEngineVersionService: CoreEngineVersionService, workspaceVersionService: WorkspaceVersionService, - registeredInstanceMigrationService: RegisteredInstanceMigrationService, + upgradeCommandRegistryService: UpgradeCommandRegistryService, instanceUpgradeService: InstanceUpgradeService, workspaceIteratorService: WorkspaceIteratorService, workspaceUpgradeService: WorkspaceUpgradeService, @@ -125,7 +119,7 @@ const buildUpgradeCommandModule = async ({ return new commandRunner( coreEngineVersionService, workspaceVersionService, - registeredInstanceMigrationService, + upgradeCommandRegistryService, instanceUpgradeService, workspaceIteratorService, workspaceUpgradeService, @@ -135,7 +129,7 @@ const buildUpgradeCommandModule = async ({ inject: [ CoreEngineVersionService, WorkspaceVersionService, - RegisteredInstanceMigrationService, + UpgradeCommandRegistryService, InstanceUpgradeService, WorkspaceIteratorService, WorkspaceUpgradeService, @@ -240,7 +234,7 @@ const buildUpgradeCommandModule = async ({ }; describe('UpgradeCommandRunner', () => { - let upgradeCommandRunner: BasicUpgradeCommandRunner; + let upgradeCommandRunner: UpgradeCommand; type BuildModuleAndSetupSpiesArgs = { numberOfWorkspace?: number; @@ -254,7 +248,7 @@ describe('UpgradeCommandRunner', () => { numberOfWorkspace = 1, workspaceOverride, workspaces, - commandRunner = BasicUpgradeCommandRunner, + commandRunner = UpgradeCommand, appVersion = CURRENT_VERSION, migrations, }: BuildModuleAndSetupSpiesArgs) => { @@ -372,19 +366,19 @@ describe('UpgradeCommandRunner', () => { }); it('should call runSingleMigration for each current-version instance command', async () => { - @RegisteredInstanceMigration(CURRENT_VERSION, 1770000000000) + @RegisteredInstanceCommand(CURRENT_VERSION, 1770000000000) class AddIndexToUsers1770000000000 implements MigrationInterface { async up(_queryRunner: QueryRunner) {} async down(_queryRunner: QueryRunner) {} } - @RegisteredInstanceMigration(CURRENT_VERSION, 1771000000000) + @RegisteredInstanceCommand(CURRENT_VERSION, 1771000000000) class AddColumnToAccounts1771000000000 implements MigrationInterface { async up(_queryRunner: QueryRunner) {} async down(_queryRunner: QueryRunner) {} } - @RegisteredInstanceMigration(PREVIOUS_VERSION, 1769000000000) + @RegisteredInstanceCommand(PREVIOUS_VERSION, 1769000000000) class DropLegacyTable1769000000000 implements MigrationInterface { async up(_queryRunner: QueryRunner) {} async down(_queryRunner: QueryRunner) {} @@ -423,7 +417,7 @@ describe('UpgradeCommandRunner', () => { }); it('should skip already-executed instance commands', async () => { - @RegisteredInstanceMigration(CURRENT_VERSION, 1770000000000) + @RegisteredInstanceCommand(CURRENT_VERSION, 1770000000000) class AlreadyRunMigration1770000000000 implements MigrationInterface { async up(_queryRunner: QueryRunner) {} async down(_queryRunner: QueryRunner) {} @@ -452,7 +446,7 @@ describe('UpgradeCommandRunner', () => { }); it('should throw when a migration fails', async () => { - @RegisteredInstanceMigration(CURRENT_VERSION, 1770000000000) + @RegisteredInstanceCommand(CURRENT_VERSION, 1770000000000) class FailingMigration1770000000000 implements MigrationInterface { async up(_queryRunner: QueryRunner) {} async down(_queryRunner: QueryRunner) {} @@ -480,7 +474,7 @@ describe('UpgradeCommandRunner', () => { }); it('should log success when a migration succeeds', async () => { - @RegisteredInstanceMigration(CURRENT_VERSION, 1770000000000) + @RegisteredInstanceCommand(CURRENT_VERSION, 1770000000000) class SuccessMigration1770000000000 implements MigrationInterface { async up(_queryRunner: QueryRunner) {} async down(_queryRunner: QueryRunner) {} @@ -546,16 +540,6 @@ describe('UpgradeCommandRunner', () => { 'APP_VERSION is not defined, please double check your env variables', }, }, - { - title: 'when current version commands are not found', - context: { - input: { - appVersion: '42.0.0', - }, - expectedErrorMessage: - 'No command found for version 42.0.0. Please check the commands record.', - }, - }, { title: 'when previous version is not found', context: { diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade-version-command.module.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade-version-command.module.ts index 627d5256d6..8a5f0ce6c7 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade-version-command.module.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade-version-command.module.ts @@ -1,7 +1,6 @@ import { Module } from '@nestjs/common'; import { WorkspaceIteratorModule } from 'src/database/commands/command-runners/workspace-iterator.module'; -import { V1_20_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-20/1-20-upgrade-version-command.module'; import { V1_21_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-21/1-21-upgrade-version-command.module'; import { UpgradeCommand } from 'src/database/commands/upgrade-version-command/upgrade.command'; import { CoreEngineVersionModule } from 'src/engine/core-engine-version/core-engine-version.module'; @@ -11,7 +10,6 @@ import { WorkspaceVersionModule } from 'src/engine/workspace-manager/workspace-v @Module({ imports: [ - V1_20_UpgradeVersionCommandModule, V1_21_UpgradeVersionCommandModule, DataSourceModule, CoreEngineVersionModule, diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts index 130328c029..0215e223c0 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts @@ -1,146 +1,316 @@ import { InjectDataSource } from '@nestjs/typeorm'; -import { Command } from 'nest-commander'; -import { DataSource } from 'typeorm'; +import chalk from 'chalk'; +import { Command, CommandRunner, Option } from 'nest-commander'; +import { SemVer } from 'semver'; +import { assertUnreachable, isDefined } from 'twenty-shared/utils'; +import { DataSource, MigrationInterface } from 'typeorm'; -import { - type AllCommands, - UpgradeCommandRunner, - type VersionCommands, -} from 'src/database/commands/command-runners/upgrade.command-runner'; +import { ActiveOrSuspendedWorkspaceCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspace.command-runner'; import { WorkspaceIteratorService } from 'src/database/commands/command-runners/workspace-iterator.service'; -import { BackfillCommandMenuItemsCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-backfill-command-menu-items.command'; -import { BackfillNavigationMenuItemTypeCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-backfill-navigation-menu-item-type.command'; -import { BackfillSelectFieldOptionIdsCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-backfill-select-field-option-ids.command'; -import { DeleteOrphanNavigationMenuItemsCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-delete-orphan-navigation-menu-items.command'; -import { IdentifyFieldPermissionMetadataCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-identify-field-permission-metadata.command'; -import { IdentifyObjectPermissionMetadataCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-identify-object-permission-metadata.command'; -import { IdentifyPermissionFlagMetadataCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-identify-permission-flag-metadata.command'; -import { MakeFieldPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-make-field-permission-universal-identifier-and-application-id-not-nullable-migration.command'; -import { MakeObjectPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-make-object-permission-universal-identifier-and-application-id-not-nullable-migration.command'; -import { MakePermissionFlagUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-make-permission-flag-universal-identifier-and-application-id-not-nullable-migration.command'; -import { MakeWorkflowSearchableCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-make-workflow-searchable.command'; -import { MigrateMessagingInfrastructureToMetadataCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-migrate-messaging-infrastructure-to-metadata.command'; -import { MigrateRichTextToTextCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-migrate-rich-text-to-text.command'; -import { SeedCliApplicationRegistrationCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-seed-cli-application-registration.command'; -import { UpdateStandardIndexViewNamesCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-update-standard-index-view-names.command'; - -import { AddComposeEmailCommandMenuItemCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-add-compose-email-command-menu-item.command'; -import { RefactorNavigationCommandsCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-refactor-navigation-commands.command'; -import { AddGlobalKeyValuePairUniqueIndexCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-add-global-key-value-pair-unique-index.command'; -import { BackfillDatasourceToWorkspaceCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-backfill-datasource-to-workspace.command'; -import { BackfillMessageThreadSubjectCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-backfill-message-thread-subject.command'; -import { BackfillPageLayoutsAndFieldsWidgetViewFieldsCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-backfill-page-layouts-and-fields-widget-view-fields.command'; -import { DeduplicateEngineCommandsCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-deduplicate-engine-commands.command'; -import { DropWorkspaceMessagingFksCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-drop-workspace-messaging-fks.command'; -import { FixSelectAllCommandMenuItemsCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-fix-select-all-command-menu-items.command'; -import { MigrateAiAgentTextToJsonResponseFormatCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-migrate-ai-agent-text-to-json-response-format.command'; -import { MigrateMessageFolderParentIdToExternalIdCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-migrate-message-folder-parent-id-to-external-id.command'; -import { UpdateEditLayoutCommandMenuItemLabelCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-update-edit-layout-command-menu-item-label.command'; +import { WorkspaceCommandRunner } from 'src/database/commands/command-runners/workspace.command-runner'; +import { CommandLogger } from 'src/database/commands/logger'; +import { type UpgradeCommandVersion } from 'src/engine/constants/upgrade-command-supported-versions.constant'; import { CoreEngineVersionService } from 'src/engine/core-engine-version/services/core-engine-version.service'; 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 { WorkspaceUpgradeService } from 'src/engine/core-modules/upgrade/services/workspace-upgrade.service'; import { WorkspaceVersionService } from 'src/engine/workspace-manager/workspace-version/services/workspace-version.service'; +export type VersionCommands = ( + | WorkspaceCommandRunner + | ActiveOrSuspendedWorkspaceCommandRunner +)[]; + +export type UpgradeCommandOptions = { + workspaceId?: Set; + startFromWorkspaceId?: string; + workspaceCountLimit?: number; + dryRun?: boolean; + verbose?: boolean; +}; + +type VersionContext = { + fromWorkspaceVersion: SemVer; + currentAppVersion: SemVer; + currentVersionMajorMinor: UpgradeCommandVersion; + instanceCommands: MigrationInterface[]; + workspaceCommands: VersionCommands; +}; + @Command({ name: 'upgrade', description: 'Upgrade workspaces to the latest version', }) -export class UpgradeCommand extends UpgradeCommandRunner { - override allCommands: AllCommands; +export class UpgradeCommand extends CommandRunner { + protected logger: CommandLogger; constructor( protected readonly coreEngineVersionService: CoreEngineVersionService, protected readonly workspaceVersionService: WorkspaceVersionService, - protected readonly registeredInstanceMigrationService: RegisteredInstanceMigrationService, + protected readonly upgradeCommandRegistryService: UpgradeCommandRegistryService, protected readonly instanceUpgradeService: InstanceUpgradeService, protected readonly workspaceIteratorService: WorkspaceIteratorService, protected readonly workspaceUpgradeService: WorkspaceUpgradeService, @InjectDataSource() protected readonly dataSource: DataSource, - - // 1.20 Commands - private readonly identifyPermissionFlagMetadataCommand: IdentifyPermissionFlagMetadataCommand, - private readonly makePermissionFlagUniversalIdentifierAndApplicationIdNotNullableMigrationCommand: MakePermissionFlagUniversalIdentifierAndApplicationIdNotNullableMigrationCommand, - private readonly identifyObjectPermissionMetadataCommand: IdentifyObjectPermissionMetadataCommand, - private readonly makeObjectPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand: MakeObjectPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand, - private readonly identifyFieldPermissionMetadataCommand: IdentifyFieldPermissionMetadataCommand, - private readonly makeFieldPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand: MakeFieldPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand, - private readonly backfillNavigationMenuItemTypeCommand: BackfillNavigationMenuItemTypeCommand, - private readonly backfillCommandMenuItemsCommand: BackfillCommandMenuItemsCommand, - private readonly deleteOrphanNavigationMenuItemsCommand: DeleteOrphanNavigationMenuItemsCommand, - private readonly seedCliApplicationRegistrationCommand: SeedCliApplicationRegistrationCommand, - private readonly migrateRichTextToTextCommand: MigrateRichTextToTextCommand, - private readonly migrateMessagingInfrastructureToMetadataCommand: MigrateMessagingInfrastructureToMetadataCommand, - private readonly backfillSelectFieldOptionIdsCommand: BackfillSelectFieldOptionIdsCommand, - private readonly updateStandardIndexViewNamesCommand: UpdateStandardIndexViewNamesCommand, - private readonly makeWorkflowSearchableCommand: MakeWorkflowSearchableCommand, - - // 1.21 Commands - private readonly addComposeEmailCommandMenuItemCommand: AddComposeEmailCommandMenuItemCommand, - private readonly addGlobalKeyValuePairUniqueIndexCommand: AddGlobalKeyValuePairUniqueIndexCommand, - private readonly backfillDatasourceToWorkspaceCommand: BackfillDatasourceToWorkspaceCommand, - private readonly backfillMessageThreadSubjectCommand: BackfillMessageThreadSubjectCommand, - private readonly backfillPageLayoutsAndFieldsWidgetViewFieldsCommand: BackfillPageLayoutsAndFieldsWidgetViewFieldsCommand, - private readonly deduplicateEngineCommandsCommand: DeduplicateEngineCommandsCommand, - private readonly fixSelectAllCommandMenuItemsCommand: FixSelectAllCommandMenuItemsCommand, - private readonly migrateAiAgentTextToJsonResponseFormatCommand: MigrateAiAgentTextToJsonResponseFormatCommand, - private readonly updateEditLayoutCommandMenuItemLabelCommand: UpdateEditLayoutCommandMenuItemLabelCommand, - private readonly refactorNavigationCommandsCommand: RefactorNavigationCommandsCommand, - private readonly dropWorkspaceMessagingFksCommand: DropWorkspaceMessagingFksCommand, - private readonly migrateMessageFolderParentIdToExternalIdCommand: MigrateMessageFolderParentIdToExternalIdCommand, ) { - super( - coreEngineVersionService, - workspaceVersionService, - registeredInstanceMigrationService, - instanceUpgradeService, - workspaceIteratorService, - workspaceUpgradeService, - dataSource, - ); + super(); + this.logger = new CommandLogger({ + verbose: false, + constructorName: this.constructor.name, + }); + } - const commands_1200: VersionCommands = [ - this.identifyPermissionFlagMetadataCommand, - this - .makePermissionFlagUniversalIdentifierAndApplicationIdNotNullableMigrationCommand, - this.identifyObjectPermissionMetadataCommand, - this - .makeObjectPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand, - this.identifyFieldPermissionMetadataCommand, - this - .makeFieldPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand, - this.backfillNavigationMenuItemTypeCommand, - this.migrateRichTextToTextCommand, - this.deleteOrphanNavigationMenuItemsCommand, - this.backfillCommandMenuItemsCommand, - this.seedCliApplicationRegistrationCommand, - this.migrateMessagingInfrastructureToMetadataCommand, - this.backfillSelectFieldOptionIdsCommand, - this.updateStandardIndexViewNamesCommand, - this.makeWorkflowSearchableCommand, - ]; + @Option({ + flags: '-d, --dry-run', + description: 'Simulate the command without making actual changes', + required: false, + }) + parseDryRun(): boolean { + return true; + } - const commands_1210: VersionCommands = [ - this.addComposeEmailCommandMenuItemCommand, - this.addGlobalKeyValuePairUniqueIndexCommand, - this.backfillDatasourceToWorkspaceCommand, - this.backfillMessageThreadSubjectCommand, - this.backfillPageLayoutsAndFieldsWidgetViewFieldsCommand, - this.deduplicateEngineCommandsCommand, - this.fixSelectAllCommandMenuItemsCommand, - this.migrateAiAgentTextToJsonResponseFormatCommand, - this.updateEditLayoutCommandMenuItemLabelCommand, - this.refactorNavigationCommandsCommand, - this.dropWorkspaceMessagingFksCommand, - this.migrateMessageFolderParentIdToExternalIdCommand, - ]; + @Option({ + flags: '-v, --verbose', + description: 'Verbose output', + required: false, + }) + parseVerbose(): boolean { + return true; + } - this.allCommands = { - '1.19.0': [], - '1.20.0': commands_1200, - '1.21.0': commands_1210, + @Option({ + flags: '-w, --workspace-id [workspace_id]', + description: + 'workspace id. Command runs on all active/suspended workspaces if not provided.', + required: false, + }) + parseWorkspaceId(val: string, previous?: Set): Set { + const accumulator = previous ?? new Set(); + + accumulator.add(val); + + return accumulator; + } + + @Option({ + flags: '--start-from-workspace-id [workspace_id]', + description: + 'Start from a specific workspace id. Workspaces are processed in ascending order of id.', + required: false, + }) + parseStartFromWorkspaceId(val: string): string { + return val; + } + + @Option({ + flags: '--workspace-count-limit [count]', + description: + 'Limit the number of workspaces to process. Workspaces are processed in ascending order of id.', + required: false, + }) + parseWorkspaceCountLimit(val: string): number { + const limit = parseInt(val); + + if (isNaN(limit)) { + throw new Error('Workspace count limit must be a number'); + } + + if (limit <= 0) { + throw new Error('Workspace count limit must be greater than 0'); + } + + return limit; + } + + override async run( + _passedParams: string[], + options: UpgradeCommandOptions, + ): Promise { + if (options.verbose) { + this.logger = new CommandLogger({ + verbose: true, + constructorName: this.constructor.name, + }); + } + + try { + const versionContext = this.resolveVersionContext(); + this.logger.log( + chalk.blue( + [ + 'Initialized upgrade context with:', + `- currentVersion (migrating to): ${versionContext.currentAppVersion}`, + `- fromWorkspaceVersion: ${versionContext.fromWorkspaceVersion}`, + `- ${versionContext.instanceCommands.length} instance commands (from registry)`, + `- ${versionContext.workspaceCommands.length} workspace commands`, + ].join('\n '), + ), + ); + + const workspacesBelowMinimumVersion = + await this.workspaceVersionService.getWorkspacesBelowVersion( + versionContext.fromWorkspaceVersion.version, + ); + + if (workspacesBelowMinimumVersion.length > 0) { + const ineligibleIds = workspacesBelowMinimumVersion + .map((workspace) => workspace.id) + .join(', '); + + throw new Error( + `Unable to run the upgrade command. Aborting the upgrade process. +Workspaces below minimum version (${versionContext.fromWorkspaceVersion.version}): ${ineligibleIds}. +Please roll back to that version and run the upgrade command again.`, + ); + } + + await this.runLegacyPendingTypeOrmMigrations(); + await this.runInstanceCommandsOrThrow(versionContext); + + const hasWorkspaces = + await this.workspaceVersionService.hasActiveOrSuspendedWorkspaces(); + + if (!hasWorkspaces) { + this.logger.log( + chalk.blue( + 'Fresh installation detected, skipping workspace commands', + ), + ); + + return; + } + + const iteratorReport = await this.runWorkspaceCommands( + options, + versionContext, + ); + + this.logger.log( + chalk.blue( + `Upgrade summary: ${iteratorReport.success.length} succeeded, ${iteratorReport.fail.length} failed`, + ), + ); + + if (iteratorReport.fail.length > 0) { + throw new Error( + `Upgrade completed with ${iteratorReport.fail.length} workspace failure(s)`, + ); + } + } catch (error) { + this.logger.error(chalk.red(`Upgrade failed: ${error.message}`)); + throw error; + } + } + + private async runLegacyPendingTypeOrmMigrations(): Promise { + this.logger.log('Running legacy TypeORM migrations...'); + + const migrations = await this.dataSource.runMigrations({ + transaction: 'each', + }); + + if (migrations.length === 0) { + this.logger.log('No pending legacy migrations'); + } else { + this.logger.log( + `Executed ${migrations.length} legacy migration(s): ${migrations.map((migration) => migration.name).join(', ')}`, + ); + } + } + + private async runInstanceCommandsOrThrow( + versionContext: VersionContext, + ): Promise { + for (const instanceCommand of versionContext.instanceCommands) { + const migrationName = instanceCommand.constructor.name; + const result = + await this.instanceUpgradeService.runSingleMigration(instanceCommand); + + switch (result.status) { + case 'already-executed': { + this.logger.warn( + `Core migration ${migrationName} already executed, skipping`, + ); + + break; + } + case 'failed': { + this.logger.error(`Core migration ${migrationName} failed`); + + if (isDefined(result.error)) { + this.logger.error( + result.error instanceof Error + ? (result.error.stack ?? result.error.message) + : String(result.error), + ); + } + + throw new Error(`Core migration ${migrationName} failed`); + } + case 'success': { + this.logger.log( + `Core migration ${migrationName} executed successfully`, + ); + + break; + } + default: { + assertUnreachable(result); + } + } + } + } + + private resolveVersionContext(): VersionContext { + const currentAppVersion = this.coreEngineVersionService.getCurrentVersion(); + const currentVersionMajorMinor = + `${currentAppVersion.major}.${currentAppVersion.minor}.0` as UpgradeCommandVersion; + + const workspaceCommands = + this.upgradeCommandRegistryService.getWorkspaceCommandsForVersion( + currentVersionMajorMinor, + ); + + const fromWorkspaceVersion = + this.coreEngineVersionService.getPreviousVersion(); + + const instanceCommands = + this.upgradeCommandRegistryService.getInstanceCommandsForVersion( + currentVersionMajorMinor, + ); + + return { + fromWorkspaceVersion, + currentAppVersion, + currentVersionMajorMinor, + workspaceCommands, + instanceCommands, }; } + + private async runWorkspaceCommands( + options: UpgradeCommandOptions, + versionContext: VersionContext, + ) { + return await this.workspaceIteratorService.iterate({ + workspaceIds: + options.workspaceId && options.workspaceId.size > 0 + ? Array.from(options.workspaceId) + : undefined, + startFromWorkspaceId: options.startFromWorkspaceId, + workspaceCountLimit: options.workspaceCountLimit, + dryRun: options.dryRun, + callback: async (context) => { + await this.workspaceUpgradeService.upgradeWorkspace({ + iteratorContext: context, + options, + fromWorkspaceVersion: versionContext.fromWorkspaceVersion, + currentAppVersion: versionContext.currentAppVersion, + workspaceCommands: versionContext.workspaceCommands, + }); + }, + }); + } } diff --git a/packages/twenty-server/src/engine/constants/upgrade-command-supported-versions.constant.ts b/packages/twenty-server/src/engine/constants/upgrade-command-supported-versions.constant.ts index b6d6781658..4a35154f2e 100644 --- a/packages/twenty-server/src/engine/constants/upgrade-command-supported-versions.constant.ts +++ b/packages/twenty-server/src/engine/constants/upgrade-command-supported-versions.constant.ts @@ -1,8 +1,8 @@ -export const UPGRADE_COMMAND_SUPPORTED_VERSIONS = [ - '1.19.0', - '1.20.0', - '1.21.0', -] as const; +// Keep at least two entries: the current version and the one before it. +// getPreviousVersion() looks up the entry just below the current version +// to determine the minimum workspace version eligible for upgrade. +// Removing the previous version would cause the upgrade command to fail. +export const UPGRADE_COMMAND_SUPPORTED_VERSIONS = ['1.20.0', '1.21.0'] as const; export type UpgradeCommandVersion = (typeof UPGRADE_COMMAND_SUPPORTED_VERSIONS)[number]; diff --git a/packages/twenty-server/src/database/typeorm/core/decorators/registered-instance-migration.decorator.ts b/packages/twenty-server/src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator.ts similarity index 52% rename from packages/twenty-server/src/database/typeorm/core/decorators/registered-instance-migration.decorator.ts rename to packages/twenty-server/src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator.ts index b7d9f5057a..dc7ed58dd2 100644 --- a/packages/twenty-server/src/database/typeorm/core/decorators/registered-instance-migration.decorator.ts +++ b/packages/twenty-server/src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator.ts @@ -4,28 +4,28 @@ import { Injectable } from '@nestjs/common'; import { type UpgradeCommandVersion } from 'src/engine/constants/upgrade-command-supported-versions.constant'; -export type RegisteredInstanceMigrationMetadata = { +export type RegisteredInstanceCommandMetadata = { version: UpgradeCommandVersion; timestamp: number; }; -const REGISTERED_INSTANCE_MIGRATION_KEY = 'REGISTERED_INSTANCE_MIGRATION'; +const REGISTERED_INSTANCE_COMMAND_KEY = 'REGISTERED_INSTANCE_COMMAND'; // When dropping a version from UPGRADE_COMMAND_SUPPORTED_VERSIONS, also -// remove the @RegisteredInstanceMigration decorator from its associated -// migration files. -export const RegisteredInstanceMigration = +// remove the @RegisteredInstanceCommand decorator from its associated +// command files. +export const RegisteredInstanceCommand = (version: UpgradeCommandVersion, timestamp: number): ClassDecorator => (target) => { Injectable()(target); Reflect.defineMetadata( - REGISTERED_INSTANCE_MIGRATION_KEY, + REGISTERED_INSTANCE_COMMAND_KEY, { version, timestamp }, target, ); }; -export const getRegisteredInstanceMigrationMetadata = ( +export const getRegisteredInstanceCommandMetadata = ( target: Function, -): RegisteredInstanceMigrationMetadata | undefined => - Reflect.getMetadata(REGISTERED_INSTANCE_MIGRATION_KEY, target); +): RegisteredInstanceCommandMetadata | undefined => + Reflect.getMetadata(REGISTERED_INSTANCE_COMMAND_KEY, target); diff --git a/packages/twenty-server/src/engine/core-modules/upgrade/decorators/registered-workspace-command.decorator.ts b/packages/twenty-server/src/engine/core-modules/upgrade/decorators/registered-workspace-command.decorator.ts new file mode 100644 index 0000000000..1f60bf86f3 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/upgrade/decorators/registered-workspace-command.decorator.ts @@ -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); diff --git a/packages/twenty-server/src/engine/core-modules/upgrade/services/__tests__/registered-instance-migration-registry.service.spec.ts b/packages/twenty-server/src/engine/core-modules/upgrade/services/__tests__/registered-instance-migration-registry.service.spec.ts deleted file mode 100644 index b9a61c926a..0000000000 --- a/packages/twenty-server/src/engine/core-modules/upgrade/services/__tests__/registered-instance-migration-registry.service.spec.ts +++ /dev/null @@ -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 {} - async down(): Promise {} -} - -@RegisteredInstanceMigration('1.21.0', 1771000000000) -class MigrationB1771000000000 implements MigrationInterface { - name = 'MigrationB1771000000000'; - - async up(): Promise {} - async down(): Promise {} -} - -@RegisteredInstanceMigration('1.21.0', 1772000000000) -class MigrationC1772000000000 implements MigrationInterface { - name = 'MigrationC1772000000000'; - - async up(): Promise {} - async down(): Promise {} -} - -@RegisteredInstanceMigration('1.20.0', 1769000000000) -class MigrationD1769000000000 implements MigrationInterface { - name = 'MigrationD1769000000000'; - - async up(): Promise {} - async down(): Promise {} -} - -class UndecoratedMigration1768000000000 implements MigrationInterface { - name = 'UndecoratedMigration1768000000000'; - - async up(): Promise {} - async down(): Promise {} -} - -const buildProviderWrapper = (migration: MigrationInterface) => ({ - instance: migration, - metatype: migration.constructor, -}); - -const buildRegistryService = async ( - migrations: MigrationInterface[], -): Promise => { - 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([]); - }); -}); diff --git a/packages/twenty-server/src/engine/core-modules/upgrade/services/__tests__/upgrade-command-registry.service.spec.ts b/packages/twenty-server/src/engine/core-modules/upgrade/services/__tests__/upgrade-command-registry.service.spec.ts new file mode 100644 index 0000000000..3836e04324 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/upgrade/services/__tests__/upgrade-command-registry.service.spec.ts @@ -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 {} + async down(): Promise {} +} + +@RegisteredInstanceCommand('1.21.0', 1771000000000) +class MigrationB1771000000000 implements MigrationInterface { + name = 'MigrationB1771000000000'; + + async up(): Promise {} + async down(): Promise {} +} + +@RegisteredInstanceCommand('1.21.0', 1772000000000) +class MigrationC1772000000000 implements MigrationInterface { + name = 'MigrationC1772000000000'; + + async up(): Promise {} + async down(): Promise {} +} + +@RegisteredInstanceCommand('1.20.0', 1769000000000) +class MigrationD1769000000000 implements MigrationInterface { + name = 'MigrationD1769000000000'; + + async up(): Promise {} + async down(): Promise {} +} + +class UndecoratedMigration1768000000000 implements MigrationInterface { + name = 'UndecoratedMigration1768000000000'; + + async up(): Promise {} + async down(): Promise {} +} + +@RegisteredWorkspaceCommand('1.21.0', 1773000000000) +class WorkspaceCommandA { + async runOnWorkspace(): Promise {} +} + +@RegisteredWorkspaceCommand('1.21.0', 1774000000000) +class WorkspaceCommandB { + async runOnWorkspace(): Promise {} +} + +const buildProviderWrapper = (instance: object) => ({ + instance, + metatype: instance.constructor, +}); + +const buildRegistryService = async ( + instances: object[], +): Promise => { + 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 {} + } + + 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 {} + async down(): Promise {} + } + + 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 {} + } + + 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 {} + } + + 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); + }); +}); diff --git a/packages/twenty-server/src/engine/core-modules/upgrade/services/registered-instance-migration-registry.service.ts b/packages/twenty-server/src/engine/core-modules/upgrade/services/registered-instance-migration-registry.service.ts deleted file mode 100644 index f2f0e5929e..0000000000 --- a/packages/twenty-server/src/engine/core-modules/upgrade/services/registered-instance-migration-registry.service.ts +++ /dev/null @@ -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; - } -} diff --git a/packages/twenty-server/src/engine/core-modules/upgrade/services/upgrade-command-registry.service.ts b/packages/twenty-server/src/engine/core-modules/upgrade/services/upgrade-command-registry.service.ts new file mode 100644 index 0000000000..fb10727367 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/upgrade/services/upgrade-command-registry.service.ts @@ -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(); + + 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(); + + 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); + } + } +} diff --git a/packages/twenty-server/src/engine/core-modules/upgrade/services/workspace-upgrade.service.ts b/packages/twenty-server/src/engine/core-modules/upgrade/services/workspace-upgrade.service.ts index 80828fa649..2aa4847f74 100644 --- a/packages/twenty-server/src/engine/core-modules/upgrade/services/workspace-upgrade.service.ts +++ b/packages/twenty-server/src/engine/core-modules/upgrade/services/workspace-upgrade.service.ts @@ -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'; diff --git a/packages/twenty-server/src/engine/core-modules/upgrade/upgrade.module.ts b/packages/twenty-server/src/engine/core-modules/upgrade/upgrade.module.ts index 6fab11052b..8d3f941f58 100644 --- a/packages/twenty-server/src/engine/core-modules/upgrade/upgrade.module.ts +++ b/packages/twenty-server/src/engine/core-modules/upgrade/upgrade.module.ts @@ -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 {}