From c0eacedfec5ccf4964401c384d9cfbe9c9728a27 Mon Sep 17 00:00:00 2001 From: Paul Rastoin <45004772+prastoin@users.noreply.github.com> Date: Tue, 7 Apr 2026 18:33:48 +0200 Subject: [PATCH] Workspace command decorators (#19397) # Introduction Migrating the workspace commands to the decorator version + timestamp listing as for the instance commands We've now been able to remove the upgrade command abstraction where we needed to import all modules and order them Now they're dynamically retrieved at upgrade runtime, sorted by timestamp ## Instance and workspace commands name The name is computed from the command metadata `version` `className` and `timestamp` we have a duplicate validation at module init from the unified registry --- packages/twenty-oxlint-rules/oxlint-plugin.ts | 5 + .../rules/upgrade-command-filename.spec.ts | 88 ++++ .../rules/upgrade-command-filename.ts | 100 +++++ packages/twenty-server/.oxlintrc.json | 1 + ...ce-command-generation.service.spec.ts.snap | 72 ++-- ...nstance-command-generation.service.spec.ts | 2 +- .../command-runners/upgrade.command-runner.ts | 314 -------------- .../generate-instance-command.command.ts | 2 +- .../instance-command-generation.service.ts | 18 +- .../commands/run-instance-commands.command.ts | 6 +- ...-20-backfill-command-menu-items.command.ts | 379 ---------------- ...kfill-navigation-menu-item-type.command.ts | 130 ------ ...ackfill-select-field-option-ids.command.ts | 94 ---- ...te-orphan-navigation-menu-items.command.ts | 81 ---- ...ntify-field-permission-metadata.command.ts | 106 ----- ...tify-object-permission-metadata.command.ts | 106 ----- ...entify-permission-flag-metadata.command.ts | 107 ----- ...ation-id-not-nullable-migration.command.ts | 67 --- ...ation-id-not-nullable-migration.command.ts | 67 --- ...ation-id-not-nullable-migration.command.ts | 67 --- .../1-20-make-workflow-searchable.command.ts | 82 ---- ...ging-infrastructure-to-metadata.command.ts | 370 ---------------- .../1-20-migrate-rich-text-to-text.command.ts | 155 ------- ...ed-cli-application-registration.command.ts | 53 --- ...pdate-standard-index-view-names.command.ts | 83 ---- .../1-20-upgrade-version-command.module.ts | 96 ----- .../1-21-upgrade-version-command.module.ts | 41 +- ...ompose-email-command-menu-item.command.ts} | 2 + ...al-key-value-pair-unique-index.command.ts} | 2 + ...ckfill-datasource-to-workspace.command.ts} | 2 + ...ackfill-message-thread-subject.command.ts} | 2 + ...-and-fields-widget-view-fields.command.ts} | 2 + ...00-deduplicate-engine-commands.command.ts} | 2 + ...-select-all-command-menu-items.command.ts} | 2 + ...t-text-to-json-response-format.command.ts} | 2 + ...layout-command-menu-item-label.command.ts} | 2 + ...0-drop-workspace-messaging-fks.command.ts} | 2 + ...older-parent-id-to-external-id.command.ts} | 2 + ...ing-infrastructure-to-metadata.command.ts} | 2 + ...0-refactor-navigation-commands.command.ts} | 2 + .../__tests__/upgrade.command.spec.ts} | 62 +-- .../upgrade-version-command.module.ts | 2 - .../upgrade.command.ts | 404 +++++++++++++----- ...ade-command-supported-versions.constant.ts | 10 +- .../registered-instance-command.decorator.ts} | 18 +- .../registered-workspace-command.decorator.ts | 25 ++ ...nstance-migration-registry.service.spec.ts | 145 ------- .../upgrade-command-registry.service.spec.ts | 293 +++++++++++++ ...red-instance-migration-registry.service.ts | 100 ----- .../upgrade-command-registry.service.ts | 228 ++++++++++ .../services/workspace-upgrade.service.ts | 4 +- .../core-modules/upgrade/upgrade.module.ts | 6 +- 52 files changed, 1147 insertions(+), 2868 deletions(-) create mode 100644 packages/twenty-oxlint-rules/rules/upgrade-command-filename.spec.ts create mode 100644 packages/twenty-oxlint-rules/rules/upgrade-command-filename.ts delete mode 100644 packages/twenty-server/src/database/commands/command-runners/upgrade.command-runner.ts delete mode 100644 packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-backfill-command-menu-items.command.ts delete mode 100644 packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-backfill-navigation-menu-item-type.command.ts delete mode 100644 packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-backfill-select-field-option-ids.command.ts delete mode 100644 packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-delete-orphan-navigation-menu-items.command.ts delete mode 100644 packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-identify-field-permission-metadata.command.ts delete mode 100644 packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-identify-object-permission-metadata.command.ts delete mode 100644 packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-identify-permission-flag-metadata.command.ts delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 delete mode 100644 packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-make-workflow-searchable.command.ts delete mode 100644 packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-migrate-messaging-infrastructure-to-metadata.command.ts delete mode 100644 packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-migrate-rich-text-to-text.command.ts delete mode 100644 packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-seed-cli-application-registration.command.ts delete mode 100644 packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-update-standard-index-view-names.command.ts delete mode 100644 packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-upgrade-version-command.module.ts rename packages/twenty-server/src/database/commands/upgrade-version-command/1-21/{1-21-add-compose-email-command-menu-item.command.ts => 1-21-workspace-command-1775500001000-add-compose-email-command-menu-item.command.ts} (96%) rename packages/twenty-server/src/database/commands/upgrade-version-command/1-21/{1-21-workspace-command-add-global-key-value-pair-unique-index.command.ts => 1-21-workspace-command-1775500002000-add-global-key-value-pair-unique-index.command.ts} (94%) rename packages/twenty-server/src/database/commands/upgrade-version-command/1-21/{1-21-workspace-command-backfill-datasource-to-workspace.command.ts => 1-21-workspace-command-1775500003000-backfill-datasource-to-workspace.command.ts} (94%) rename packages/twenty-server/src/database/commands/upgrade-version-command/1-21/{1-21-workspace-command-backfill-message-thread-subject.command.ts => 1-21-workspace-command-1775500004000-backfill-message-thread-subject.command.ts} (97%) rename 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 => 1-21-workspace-command-1775500005000-backfill-page-layouts-and-fields-widget-view-fields.command.ts} (99%) rename packages/twenty-server/src/database/commands/upgrade-version-command/1-21/{1-21-workspace-command-deduplicate-engine-commands.command.ts => 1-21-workspace-command-1775500006000-deduplicate-engine-commands.command.ts} (97%) rename packages/twenty-server/src/database/commands/upgrade-version-command/1-21/{1-21-workspace-command-fix-select-all-command-menu-items.command.ts => 1-21-workspace-command-1775500007000-fix-select-all-command-menu-items.command.ts} (96%) rename 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 => 1-21-workspace-command-1775500008000-migrate-ai-agent-text-to-json-response-format.command.ts} (97%) rename packages/twenty-server/src/database/commands/upgrade-version-command/1-21/{1-21-workspace-command-update-edit-layout-command-menu-item-label.command.ts => 1-21-workspace-command-1775500009000-update-edit-layout-command-menu-item-label.command.ts} (93%) rename packages/twenty-server/src/database/commands/upgrade-version-command/1-21/{1-21-workspace-command-drop-workspace-messaging-fks.command.ts => 1-21-workspace-command-1775500010000-drop-workspace-messaging-fks.command.ts} (94%) rename 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 => 1-21-workspace-command-1775500011000-migrate-message-folder-parent-id-to-external-id.command.ts} (94%) rename packages/twenty-server/src/database/commands/upgrade-version-command/1-21/{1-21-migrate-messaging-infrastructure-to-metadata.command.ts => 1-21-workspace-command-1775500012000-migrate-messaging-infrastructure-to-metadata.command.ts} (98%) rename packages/twenty-server/src/database/commands/upgrade-version-command/1-21/{1-21-refactor-navigation-commands.command.ts => 1-21-workspace-command-1775500013000-refactor-navigation-commands.command.ts} (98%) rename packages/twenty-server/src/database/commands/{command-runners/__tests__/upgrade.command-runner.spec.ts => upgrade-version-command/__tests__/upgrade.command.spec.ts} (90%) rename packages/twenty-server/src/{database/typeorm/core/decorators/registered-instance-migration.decorator.ts => engine/core-modules/upgrade/decorators/registered-instance-command.decorator.ts} (52%) create mode 100644 packages/twenty-server/src/engine/core-modules/upgrade/decorators/registered-workspace-command.decorator.ts delete mode 100644 packages/twenty-server/src/engine/core-modules/upgrade/services/__tests__/registered-instance-migration-registry.service.spec.ts create mode 100644 packages/twenty-server/src/engine/core-modules/upgrade/services/__tests__/upgrade-command-registry.service.spec.ts delete mode 100644 packages/twenty-server/src/engine/core-modules/upgrade/services/registered-instance-migration-registry.service.ts create mode 100644 packages/twenty-server/src/engine/core-modules/upgrade/services/upgrade-command-registry.service.ts 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 {}